Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

AuthorJakob Wakeling <[email protected]>
Date2023-07-21 07:22:31
Commit52b258d5f7839961411fef3021609a9a3c4c9c9b
Parent0893c1e6ee7e521b4f36ee08c69daf4ce211cfd3

Implement user session page and session renewal

Diffstat

M main.go | 2 ++
D res/base/header.html | 23 -----------------------
M res/index.html | 27 ++++++++++++++++++++++++++-
M res/repo_log.html | 2 +-
M res/repo_refs.html | 2 +-
M res/repo_tree.html | 2 +-
M res/res.go | 13 ++++++++-----
A res/user/header.html | 15 +++++++++++++++
A res/user/sessions.html | 29 +++++++++++++++++++++++++++++
M src/admin.go | 12 ++++++------
M src/auth.go | 41 +++++++++++++++++++++++++++++++----------
M src/goit.go | 18 ++++++++++--------
M src/http.go | 7 ++++---
M src/repo.go | 4 ++--
M src/user.go | 51 ++++++++++++++++++++++++++++++++++++++++++---------

15 files changed, 178 insertions, 70 deletions

diff --git a/main.go b/main.go
index 7debc01..4783b59 100644
--- a/main.go
+++ b/main.go
@@ -24,8 +24,10 @@ func main() {
 	h.StrictSlash(true)
 
 	h.Path("/").HandlerFunc(goit.HandleIndex)
+	// h.Path("/user").Methods("GET").HandlerFunc(goit.HandleUserIndex)
 	h.Path("/user/login").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogin)
 	h.Path("/user/logout").Methods("GET", "POST").HandlerFunc(goit.HandleUserLogout)
+	h.Path("/user/sessions").Methods("GET", "POST").HandlerFunc(goit.HandleUserSessions)
 	h.Path("/repo/create").Methods("GET", "POST").HandlerFunc(goit.HandleRepoCreate)
 	h.Path("/admin").Methods("GET").HandlerFunc(goit.HandleAdminIndex)
 	h.Path("/admin/users").Methods("GET").HandlerFunc(goit.HandleAdminUsers)
diff --git a/res/base/header.html b/res/base/header.html
deleted file mode 100644
index da113bb..0000000
--- a/res/base/header.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{{define "base/header"}}
-<table>
-	<tr>
-		<td rowspan="2">
-			<a href="/"><img style="max-height: 24px;" src="/static/favicon.png"></a>
-		</td>
-		<td><h1>{{.Title}}</h1></td>
-	</tr>
-	<tr>
-		<td>
-			<a href="/">Repositories</a>
-			{{if .Admin}}
-				| <a href="/admin">Admin</a>
-			{{end}}
-			{{if .Auth}}
-				| <a href="/user/logout">Logout</a>{{if .Username}} ({{.Username}}){{end}}
-			{{else}}
-				| <a href="/user/login">Login</a>
-			{{end}}
-		</td>
-	</tr>
-</table>
-{{end}}
diff --git a/res/index.html b/res/index.html
index 628c8f6..44b60d1 100644
--- a/res/index.html
+++ b/res/index.html
@@ -1,7 +1,32 @@
 <!DOCTYPE html>
 <head>{{template "base/head" .}}</head>
 <body>
-	<header>{{template "base/header" .}}</header><hr>
+	<header>
+		<table>
+			<tr>
+				<td rowspan="2">
+					<a href="/"><img style="max-height: 24px;" src="/static/favicon.png"></a>
+				</td>
+				<td><h1>{{.Title}}</h1></td>
+			</tr>
+			<tr>
+				<td>
+					<a href="/">Repositories</a>
+					{{if .Auth}}
+						| <a href="/user/sessions">User</a>
+					{{end}}
+					{{if .Admin}}
+						| <a href="/admin">Admin</a>
+					{{end}}
+					{{if .Auth}}
+						| <a href="/user/logout">Logout</a>{{if .Username}} ({{.Username}}){{end}}
+					{{else}}
+						| <a href="/user/login">Login</a>
+					{{end}}
+				</td>
+			</tr>
+		</table>
+	</header><hr>
 	<main>
 		<table>
 			<thead>
diff --git a/res/repo_log.html b/res/repo_log.html
index 4c1aeac..155a31b 100644
--- a/res/repo_log.html
+++ b/res/repo_log.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <head>{{template "base/head" .}}</head>
 <body>
-	<header>{{template "base/repo_header" .}}</header><hr>
+	<header>{{template "repo/header" .}}</header><hr>
 	<main>
 		{{if .Commits}}
 			<table>
diff --git a/res/repo_refs.html b/res/repo_refs.html
index b250660..4b9097c 100644
--- a/res/repo_refs.html
+++ b/res/repo_refs.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <head>{{template "base/head" .}}</head>
 <body>
-	<header>{{template "base/repo_header" .}}</header><hr>
+	<header>{{template "repo/header" .}}</header><hr>
 	<main>
 	{{if .Branches}}
 		<h2>Branches</h2>
diff --git a/res/repo_tree.html b/res/repo_tree.html
index 4ca17bc..6dc6935 100644
--- a/res/repo_tree.html
+++ b/res/repo_tree.html
@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <head>{{template "base/head" .}}</head>
 <body>
-	<header>{{template "base/repo_header" .}}</header><hr>
+	<header>{{template "repo/header" .}}</header><hr>
 	<main>
 		<table>
 			<thead>
diff --git a/res/res.go b/res/res.go
index 837f577..ee395c2 100644
--- a/res/res.go
+++ b/res/res.go
@@ -11,9 +11,6 @@ var Index string
 //go:embed base/head.html
 var BaseHead string
 
-//go:embed base/header.html
-var BaseHeader string
-
 //go:embed admin/index.html
 var AdminIndex string
 
@@ -32,12 +29,18 @@ var AdminRepos string
 //go:embed admin/repo_edit.html
 var AdminRepoEdit string
 
-//go:embed repo/header.html
-var RepoHeader string
+//go:embed user/header.html
+var UserHeader string
 
 //go:embed user/login.html
 var UserLogin string
 
+//go:embed user/sessions.html
+var UserSessions string
+
+//go:embed repo/header.html
+var RepoHeader string
+
 //go:embed repo_create.html
 var RepoCreate string
 
diff --git a/res/user/header.html b/res/user/header.html
new file mode 100644
index 0000000..54e63d1
--- /dev/null
+++ b/res/user/header.html
@@ -0,0 +1,15 @@
+{{define "user/header"}}
+<table>
+	<tr>
+		<td rowspan="2">
+			<a href="/"><img style="max-height: 24px;" src="/static/favicon.png"></a>
+		</td>
+		<td><h1>{{.Title}}</h1></td>
+	</tr>
+	<tr>
+		<td>
+			<a href="/user/sessions">Sessions</a>
+		</td>
+	</tr>
+</table>
+{{end}}
diff --git a/res/user/sessions.html b/res/user/sessions.html
new file mode 100644
index 0000000..3fe4086
--- /dev/null
+++ b/res/user/sessions.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<head>{{template "base/head" .}}</head>
+<body>
+	<header>{{template "user/header" .}}</header><hr>
+	<main>
+		<table>
+			<thead>
+				<tr>
+					<td><b>IP</b></td>
+					<td><b>Seen</b></td>
+					<td><b>Expiry</b></td>
+					<td></td>
+					<td></td>
+				</tr>
+			</thead>
+			<tbody>
+			{{range .Sessions}}
+				<tr>
+					<td>{{.Ip}}</a></td>
+					<td>{{.Seen}}</td>
+					<td>{{.Expiry}}</td>
+					<td><a href="">revoke</a></td>
+					<td>{{.Current}}</td>
+				</tr>
+			{{end}}
+			</tbody>
+		</table>
+	</main>
+</body>
diff --git a/src/admin.go b/src/admin.go
index c14579a..9b98ca7 100644
--- a/src/admin.go
+++ b/src/admin.go
@@ -16,7 +16,7 @@ import (
 )
 
 func HandleAdminIndex(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -27,7 +27,7 @@ func HandleAdminIndex(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -72,7 +72,7 @@ func HandleAdminUsers(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -117,7 +117,7 @@ func HandleAdminUserCreate(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -195,7 +195,7 @@ func HandleAdminUserEdit(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminRepos(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
@@ -250,7 +250,7 @@ func HandleAdminRepos(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleAdminRepoEdit(w http.ResponseWriter, r *http.Request) {
-	if _, admin, _ := AuthCookieAdmin(r); !admin {
+	if _, admin, _ := AuthCookieAdmin(w, r, true); !admin {
 		HttpError(w, http.StatusNotFound)
 		return
 	}
diff --git a/src/auth.go b/src/auth.go
index 8a28428..4cefd53 100644
--- a/src/auth.go
+++ b/src/auth.go
@@ -9,6 +9,7 @@ import (
 	"encoding/base64"
 	"fmt"
 	"log"
+	"net"
 	"net/http"
 	"strconv"
 	"strings"
@@ -19,8 +20,8 @@ import (
 )
 
 type Session struct {
-	Token, Ip string
-	Expiry    time.Time
+	Token, Ip    string
+	Seen, Expiry time.Time
 }
 
 var Sessions = map[int64]map[string]Session{}
@@ -36,7 +37,7 @@ func NewSession(uid int64, ip string, expiry time.Time) (Session, error) {
 	}
 
 	t := base64.StdEncoding.EncodeToString(b)
-	Sessions[uid][t] = Session{t, ip, expiry}
+	Sessions[uid][t] = Session{Token: t, Ip: util.If(Conf.IpSessions, ip, ""), Seen: time.Now(), Expiry: expiry}
 	return Sessions[uid][t], nil
 }
 
@@ -65,9 +66,12 @@ func CleanupSessions() {
 }
 
 func SetSessionCookie(w http.ResponseWriter, uid int64, s Session) {
-	http.SetCookie(w, &http.Cookie{
-		Name: "session", Value: fmt.Sprint(uid) + "." + s.Token, Path: "/", Expires: s.Expiry,
-	})
+	c := &http.Cookie{Name: "session", Value: fmt.Sprint(uid) + "." + s.Token, Path: "/", Expires: s.Expiry}
+	if err := c.Valid(); err != nil {
+		log.Println("[Cookie]", err.Error())
+	}
+
+	http.SetCookie(w, c)
 }
 
 func GetSessionCookie(r *http.Request) (int64, Session) {
@@ -82,7 +86,13 @@ func GetSessionCookie(r *http.Request) (int64, Session) {
 			return -1, Session{}
 		}
 
-		return id, Sessions[id][ss[1]]
+		s := Sessions[id][ss[1]]
+		if s != (Session{}) {
+			s.Seen = time.Now()
+			Sessions[id][ss[1]] = s
+		}
+
+		return id, s
 	}
 
 	return -1, Session{}
@@ -92,9 +102,20 @@ func EndSessionCookie(w http.ResponseWriter) {
 	http.SetCookie(w, &http.Cookie{Name: "session", Path: "/", MaxAge: -1})
 }
 
-func AuthCookie(r *http.Request) (auth bool, uid int64) {
+func AuthCookie(w http.ResponseWriter, r *http.Request, renew bool) (auth bool, uid int64) {
 	if uid, s := GetSessionCookie(r); s != (Session{}) {
 		if s.Expiry.After(time.Now()) {
+			if renew && time.Until(s.Expiry) < 24*time.Hour {
+				ip, _, _ := net.SplitHostPort(r.RemoteAddr)
+				s1, err := NewSession(uid, ip, time.Now().Add(2*24*time.Hour))
+				if err != nil {
+					log.Println("[Renew Auth]", err.Error())
+				} else {
+					SetSessionCookie(w, uid, s1)
+					EndSession(uid, s.Token)
+				}
+			}
+
 			return true, uid
 		}
 
@@ -104,8 +125,8 @@ func AuthCookie(r *http.Request) (auth bool, uid int64) {
 	return false, -1
 }
 
-func AuthCookieAdmin(r *http.Request) (auth bool, admin bool, uid int64) {
-	if ok, uid := AuthCookie(r); ok {
+func AuthCookieAdmin(w http.ResponseWriter, r *http.Request, renew bool) (auth bool, admin bool, uid int64) {
+	if ok, uid := AuthCookie(w, r, renew); ok {
 		if user, err := GetUser(uid); err == nil && user.IsAdmin {
 			return true, true, uid
 		}
diff --git a/src/goit.go b/src/goit.go
index f9cb765..b71c9a1 100644
--- a/src/goit.go
+++ b/src/goit.go
@@ -18,17 +18,19 @@ import (
 )
 
 type Config struct {
-	DataPath string `json:"data_path"`
-	HttpAddr string `json:"http_addr"`
-	HttpPort string `json:"http_port"`
-	GitPath  string `json:"git_path"`
+	DataPath   string `json:"data_path"`
+	HttpAddr   string `json:"http_addr"`
+	HttpPort   string `json:"http_port"`
+	GitPath    string `json:"git_path"`
+	IpSessions bool   `json:"ip_sessions"`
 }
 
 var Conf = Config{
-	DataPath: path.Join(xdg.DataHome, "goit"),
-	HttpAddr: "",
-	HttpPort: "8080",
-	GitPath:  "git",
+	DataPath:   path.Join(xdg.DataHome, "goit"),
+	HttpAddr:   "",
+	HttpPort:   "8080",
+	GitPath:    "git",
+	IpSessions: true,
 }
 
 var db *sql.DB
diff --git a/src/http.go b/src/http.go
index 918646e..cb083cb 100644
--- a/src/http.go
+++ b/src/http.go
@@ -17,7 +17,6 @@ var tmpl = template.Must(template.New("error").Parse(res.Error))
 func init() {
 	template.Must(tmpl.New("index").Parse(res.Index))
 	template.Must(tmpl.New("base/head").Parse(res.BaseHead))
-	template.Must(tmpl.New("base/header").Parse(res.BaseHeader))
 
 	template.Must(tmpl.New("admin/index").Parse(res.AdminIndex))
 	template.Must(tmpl.New("admin/users").Parse(res.AdminUsers))
@@ -26,9 +25,11 @@ func init() {
 	template.Must(tmpl.New("admin/repos").Parse(res.AdminRepos))
 	template.Must(tmpl.New("admin/repo/edit").Parse(res.AdminRepoEdit))
 
-	template.Must(tmpl.New("base/repo_header").Parse(res.RepoHeader))
-	template.Must(tmpl.New("user_login").Parse(res.UserLogin))
+	template.Must(tmpl.New("user/header").Parse(res.UserHeader))
+	template.Must(tmpl.New("user/login").Parse(res.UserLogin))
+	template.Must(tmpl.New("user/sessions").Parse(res.UserSessions))
 
+	template.Must(tmpl.New("repo/header").Parse(res.RepoHeader))
 	template.Must(tmpl.New("repo_create").Parse(res.RepoCreate))
 
 	template.Must(tmpl.New("repo_log").Parse(res.RepoLog))
diff --git a/src/repo.go b/src/repo.go
index 0f7b260..83fc769 100644
--- a/src/repo.go
+++ b/src/repo.go
@@ -31,7 +31,7 @@ type Repo struct {
 }
 
 func HandleIndex(w http.ResponseWriter, r *http.Request) {
-	auth, admin, uid := AuthCookieAdmin(r)
+	auth, admin, uid := AuthCookieAdmin(w, r, true)
 
 	user, err := GetUser(uid)
 	if err != nil {
@@ -86,7 +86,7 @@ func HandleIndex(w http.ResponseWriter, r *http.Request) {
 }
 
 func HandleRepoCreate(w http.ResponseWriter, r *http.Request) {
-	if ok, uid := AuthCookie(r); !ok {
+	if ok, uid := AuthCookie(w, r, true); !ok {
 		HttpError(w, http.StatusUnauthorized)
 	} else if r.Method == http.MethodPost {
 		name := r.FormValue("reponame")
diff --git a/src/user.go b/src/user.go
index cd6bec8..fc967c8 100644
--- a/src/user.go
+++ b/src/user.go
@@ -10,9 +10,12 @@ import (
 	"errors"
 	"fmt"
 	"log"
+	"net"
 	"net/http"
 	"strings"
 	"time"
+
+	"github.com/Jamozed/Goit/src/util"
 )
 
 type User struct {
@@ -28,7 +31,7 @@ type User struct {
 var reserved []string = []string{"admin", "repo", "static", "user"}
 
 func HandleUserLogin(w http.ResponseWriter, r *http.Request) {
-	if ok, _ := AuthCookie(r); ok {
+	if ok, _ := AuthCookie(w, r, true); ok {
 		http.Redirect(w, r, "/", http.StatusFound)
 		return
 	}
@@ -56,18 +59,21 @@ func HandleUserLogin(w http.ResponseWriter, r *http.Request) {
 			return
 		} else if !bytes.Equal(Hash(password, u.Salt), u.Pass) {
 			data.Message = "Invalid credentials"
-		} else if s, err := NewSession(u.Id, r.RemoteAddr, time.Now().Add(15*time.Minute)); err != nil {
-			log.Println("[User:Login:Session]", err.Error())
-			HttpError(w, http.StatusInternalServerError)
-			return
 		} else {
-			SetSessionCookie(w, u.Id, s)
-			http.Redirect(w, r, "/", http.StatusFound)
-			return
+			ip, _, _ := net.SplitHostPort(r.RemoteAddr)
+			if s, err := NewSession(u.Id, ip, time.Now().Add(2*24*time.Hour)); err != nil {
+				log.Println("[User:Login:Session]", err.Error())
+				HttpError(w, http.StatusInternalServerError)
+				return
+			} else {
+				SetSessionCookie(w, u.Id, s)
+				http.Redirect(w, r, "/", http.StatusFound)
+				return
+			}
 		}
 	}
 
-	if err := tmpl.ExecuteTemplate(w, "user_login", data); err != nil {
+	if err := tmpl.ExecuteTemplate(w, "user/login", data); err != nil {
 		log.Println("[/user/login]", err.Error())
 	}
 }
@@ -79,6 +85,33 @@ func HandleUserLogout(w http.ResponseWriter, r *http.Request) {
 	http.Redirect(w, r, "/", http.StatusFound)
 }
 
+func HandleUserSessions(w http.ResponseWriter, r *http.Request) {
+	auth, uid := AuthCookie(w, r, true)
+	if !auth {
+		HttpError(w, http.StatusUnauthorized)
+		return
+	}
+
+	_, ss := GetSessionCookie(r)
+
+	type row struct{ Ip, Seen, Expiry, Current string }
+	data := struct {
+		Title    string
+		Sessions []row
+	}{Title: "User - Sessions"}
+
+	for k, v := range Sessions[uid] {
+		data.Sessions = append(data.Sessions, row{
+			Ip: v.Ip, Seen: v.Seen.Format(time.DateTime), Expiry: v.Expiry.Format(time.DateTime),
+			Current: util.If(k == ss.Token, "(current)", ""),
+		})
+	}
+
+	if err := tmpl.ExecuteTemplate(w, "user/sessions", data); err != nil {
+		log.Println("[/user/login]", err.Error())
+	}
+}
+
 func GetUser(id int64) (*User, error) {
 	u := User{}