diff --git a/hub/api/api.go b/hub/api/api.go index a0fa94e..19838b8 100644 --- a/hub/api/api.go +++ b/hub/api/api.go @@ -40,7 +40,13 @@ func New(dbPath string) (*API, error) { sessions: make(map[string]*Session), } - return a, a.ensurePassword() + if err := a.ensurePassword(); err != nil { + return nil, err + } + + go a.sweepSessions() + + return a, nil } func (a *API) ensurePassword() error { @@ -80,58 +86,72 @@ func (a *API) Session_Delete(sessionID string) error { return nil } -// Session_Get returns a snapshot copy of the session for sessionID (creating a -// fresh one if absent/expired). Returning a value rather than the stored -// pointer prevents callers from racing on the shared struct; mutations go -// through Session_SignIn / Session_Delete under the lock. +const ( + sessionTTLSecs = 86400 * 21 // sessions expire 21 days after last use + sessionSweepEvery = time.Hour // cadence of expired-session eviction +) + +// Session_Get returns a snapshot copy of the signed-in session for sessionID, +// or the zero Session if the cookie is missing/unknown/expired. It never +// creates a session, so anonymous requests cost no memory — a session is minted +// only by Session_SignIn. Returning a value (not the stored pointer) keeps +// callers from racing on the shared struct. func (a *API) Session_Get(sessionID string) (Session, error) { a.sessionsMu.Lock() defer a.sessionsMu.Unlock() - if sessionID != "" { - s, ok := a.sessions[sessionID] - if ok { - if timeSince(s.LastSeenAt) <= 86400*21 { - if timeSince(s.LastSeenAt) > 86400*7 { - s.LastSeenAt = time.Now().Unix() - } - return *s, nil - } - delete(a.sessions, sessionID) - } + s, ok := a.sessions[sessionID] + + if sessionID == "" || !ok { + return Session{}, nil } - return *a.session_Create(), nil + if timeSince(s.LastSeenAt) > sessionTTLSecs { + delete(a.sessions, sessionID) + return Session{}, nil + } + + s.LastSeenAt = time.Now().Unix() + return *s, nil } -// caller must hold sessionsMu -func (a *API) session_Create() *Session { +// Session_SignIn verifies pwd and, on success, mints a fresh signed-in session, +// returning it so the caller can set the cookie. A new ID per sign-in rotates +// the session at the privilege boundary (session-fixation resistance). +func (a *API) Session_SignIn(pwd string) (Session, error) { + conf, err := a.Config_Get() + if err != nil { + return Session{}, err + } + if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil { + return Session{}, ErrNotAuthorized + } + + a.sessionsMu.Lock() + defer a.sessionsMu.Unlock() s := &Session{ SessionID: idgen.NewToken(), + SignedIn: true, CreatedAt: time.Now().Unix(), LastSeenAt: time.Now().Unix(), } a.sessions[s.SessionID] = s - return s + return *s, nil } -func (a *API) Session_SignIn(sessionID, pwd string) error { - conf, err := a.Config_Get() - if err != nil { - return err +// sweepSessions periodically evicts sessions past their TTL. Without it, a +// signed-in session whose ID is never presented again would linger forever +// (Session_Get only evicts on a lookup of that same ID). +func (a *API) sweepSessions() { + for range time.Tick(sessionSweepEvery) { + a.sessionsMu.Lock() + for id, s := range a.sessions { + if timeSince(s.LastSeenAt) > sessionTTLSecs { + delete(a.sessions, id) + } + } + a.sessionsMu.Unlock() } - if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil { - return ErrNotAuthorized - } - a.sessionsMu.Lock() - defer a.sessionsMu.Unlock() - s, ok := a.sessions[sessionID] - if !ok { - // Session expired or was evicted between fetch and sign-in. - return ErrNotAuthorized - } - s.SignedIn = true - return nil } func (a *API) Network_Create(n *Network) error { @@ -153,7 +173,6 @@ func (a *API) Network_List() ([]*Network, error) { } func (a *API) Peer_CreateNew(p *Peer) error { - p.Version = idgen.NextID(0) p.WGPubKey = []byte{} p.SignPubKey = []byte{} p.APIKey = idgen.NewToken() @@ -175,21 +194,12 @@ func (a *API) Peer_Init(peer *Peer, args m.PeerInitArgs) error { return errors.New("peer already initialized") } - peer.Version = idgen.NextID(0) peer.WGPubKey = args.WGPubKey peer.SignPubKey = args.SignPubKey return db.Peer_UpdateFull(a.db, peer) } -func (a *API) Peer_Update(p *Peer) error { - a.lock.Lock() - defer a.lock.Unlock() - - p.Version = idgen.NextID(0) - return db.Peer_Update(a.db, p) -} - func (a *API) Peer_Delete(networkID int64, peerIP byte) error { return db.Peer_Delete(a.db, networkID, peerIP) } diff --git a/hub/api/db/generated.go b/hub/api/db/generated.go index e0ac098..22ca0f8 100644 --- a/hub/api/db/generated.go +++ b/hub/api/db/generated.go @@ -341,7 +341,6 @@ func Network_List( type Peer struct { NetworkID int64 PeerIP byte - Version int64 APIKey string Name string Addr4 []byte @@ -352,7 +351,7 @@ type Peer struct { SignPubKey []byte } -const Peer_SelectQuery = "SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey FROM peers" +const Peer_SelectQuery = "SELECT NetworkID,PeerIP,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey FROM peers" func Peer_Insert( tx TX, @@ -363,7 +362,7 @@ func Peer_Insert( return err } - _, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,Version,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey) VALUES(?,?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.Version, row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.SignPubKey) + _, err = tx.Exec("INSERT INTO peers(NetworkID,PeerIP,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey) VALUES(?,?,?,?,?,?,?,?,?,?)", row.NetworkID, row.PeerIP, row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.SignPubKey) return err } @@ -376,7 +375,7 @@ func Peer_Update( return err } - result, err := tx.Exec("UPDATE peers SET Version=?,Name=?,Addr4=?,Addr6=?,Port=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.NetworkID, row.PeerIP) + result, err := tx.Exec("UPDATE peers SET Name=?,Addr4=?,Addr6=?,Port=?,Relay=? WHERE NetworkID=? AND PeerIP=?", row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.NetworkID, row.PeerIP) if err != nil { return err } @@ -404,7 +403,7 @@ func Peer_UpdateFull( return err } - result, err := tx.Exec("UPDATE peers SET Version=?,APIKey=?,Name=?,Addr4=?,Addr6=?,Port=?,Relay=?,WGPubKey=?,SignPubKey=? WHERE NetworkID=? AND PeerIP=?", row.Version, row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.SignPubKey, row.NetworkID, row.PeerIP) + result, err := tx.Exec("UPDATE peers SET APIKey=?,Name=?,Addr4=?,Addr6=?,Port=?,Relay=?,WGPubKey=?,SignPubKey=? WHERE NetworkID=? AND PeerIP=?", row.APIKey, row.Name, row.Addr4, row.Addr6, row.Port, row.Relay, row.WGPubKey, row.SignPubKey, row.NetworkID, row.PeerIP) if err != nil { return err } @@ -456,8 +455,8 @@ func Peer_Get( err error, ) { row = &Peer{} - r := tx.QueryRow("SELECT NetworkID,PeerIP,Version,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP) - if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey); err != nil { + r := tx.QueryRow("SELECT NetworkID,PeerIP,APIKey,Name,Addr4,Addr6,Port,Relay,WGPubKey,SignPubKey FROM peers WHERE NetworkID=? AND PeerIP=?", NetworkID, PeerIP) + if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey); err != nil { row = nil } return @@ -473,7 +472,7 @@ func Peer_GetWhere( ) { row = &Peer{} r := tx.QueryRow(query, args...) - if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey); err != nil { + if err = r.Scan(&row.NetworkID, &row.PeerIP, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey); err != nil { row = nil } return @@ -495,7 +494,7 @@ func Peer_Iterate( defer rows.Close() for rows.Next() { row := &Peer{} - err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.Version, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey) + err := rows.Scan(&row.NetworkID, &row.PeerIP, &row.APIKey, &row.Name, &row.Addr4, &row.Addr6, &row.Port, &row.Relay, &row.WGPubKey, &row.SignPubKey) if !yield(row, err) { return } diff --git a/hub/api/db/tables.defs b/hub/api/db/tables.defs index e3ca04f..39665fa 100644 --- a/hub/api/db/tables.defs +++ b/hub/api/db/tables.defs @@ -12,13 +12,12 @@ TABLE networks OF Network ( TABLE peers OF Peer ( NetworkID int64 PK, PeerIP byte PK, - Version int64, APIKey string NoUpdate, - Name string, - Addr4 []byte, - Addr6 []byte, - Port uint16, - Relay bool, + Name string NoUpdate, + Addr4 []byte NoUpdate, + Addr6 []byte NoUpdate, + Port uint16 NoUpdate, + Relay bool NoUpdate, WGPubKey []byte NoUpdate, SignPubKey []byte NoUpdate ); diff --git a/hub/api/migrations/2024-11-30-init.sql b/hub/api/migrations/2024-11-30-init.sql index b443841..bce964b 100644 --- a/hub/api/migrations/2024-11-30-init.sql +++ b/hub/api/migrations/2024-11-30-init.sql @@ -12,7 +12,6 @@ CREATE TABLE networks ( CREATE TABLE peers ( NetworkID INTEGER NOT NULL, PeerIP INTEGER NOT NULL, -- Final byte of IP. - Version INTEGER NOT NULL, -- Changes when updated. APIKey TEXT NOT NULL UNIQUE, -- Peer's secret API key. Name TEXT NOT NULL, -- For humans. Addr4 BLOB NOT NULL, diff --git a/hub/handler.go b/hub/handler.go index 1581a73..7dedf05 100644 --- a/hub/handler.go +++ b/hub/handler.go @@ -20,10 +20,6 @@ func (app *App) handlePub(pattern string, fn handlerFunc) { return } - if s.SessionID != sessionID { - app.setCookie(w, SESSION_ID_COOKIE_NAME, s.SessionID) - } - if r.Method == http.MethodPost { r.ParseMultipartForm(64 * 1024) } else { diff --git a/hub/handlers.go b/hub/handlers.go index 5d30ba0..7f90f80 100644 --- a/hub/handlers.go +++ b/hub/handlers.go @@ -34,9 +34,11 @@ func (a *App) _signinSubmit(s *api.Session, w http.ResponseWriter, r *http.Reque return err } - if err := a.api.Session_SignIn(s.SessionID, pwd); err != nil { + sess, err := a.api.Session_SignIn(pwd) + if err != nil { return err } + a.setCookie(w, SESSION_ID_COOKIE_NAME, sess.SessionID) return a.redirect(w, r, "/") } @@ -185,52 +187,6 @@ func (a *App) _adminPeerView(s *api.Session, w http.ResponseWriter, r *http.Requ }{s, net, peer}) } -func (a *App) _adminPeerEdit(s *api.Session, w http.ResponseWriter, r *http.Request) error { - net, peer, err := a.formGetPeer(r.Form) - if err != nil { - return err - } - - return a.render("/network/peer-edit.html", w, struct { - Session *api.Session - Network *api.Network - Peer *api.Peer - }{s, net, peer}) -} - -func (a *App) _adminPeerEditSubmit(s *api.Session, w http.ResponseWriter, r *http.Request) error { - _, peer, err := a.formGetPeer(r.Form) - if err != nil { - return err - } - - var addr4Str, addr6Str string - - err = webutil.NewFormScanner(r.Form). - Scan("Name", &peer.Name). - Scan("Addr4", &addr4Str). - Scan("Addr6", &addr6Str). - Scan("Port", &peer.Port). - Scan("Relay", &peer.Relay). - Error() - if err != nil { - return err - } - - if peer.Addr4, err = stringToIP(addr4Str); err != nil { - return err - } - if peer.Addr6, err = stringToIP(addr6Str); err != nil { - return err - } - - if err = a.api.Peer_Update(peer); err != nil { - return err - } - - return a.redirect(w, r, "/admin/peer/view/?NetworkID=%d&PeerIP=%d", peer.NetworkID, peer.PeerIP) -} - func (a *App) _adminPeerDelete(s *api.Session, w http.ResponseWriter, r *http.Request) error { n, peer, err := a.formGetPeer(r.Form) if err != nil { @@ -378,7 +334,6 @@ func (a *App) peersList(networkID int64) (peers []m.Peer, err error) { copy(signKey[:], p.SignPubKey) peers = append(peers, m.Peer{ PeerIP: p.PeerIP, - Version: p.Version, Name: p.Name, Addr4: addrFromBytes(p.Addr4), Addr6: addrFromBytes(p.Addr6), diff --git a/hub/routes.go b/hub/routes.go index bb2d555..3ba5fe3 100644 --- a/hub/routes.go +++ b/hub/routes.go @@ -22,8 +22,6 @@ func (a *App) registerRoutes() { a.handleSignedIn("GET /admin/peer/create/", a._adminPeerCreate) a.handleSignedIn("POST /admin/peer/create/", a._adminPeerCreateSubmit) a.handleSignedIn("GET /admin/peer/view/", a._adminPeerView) - a.handleSignedIn("GET /admin/peer/edit/", a._adminPeerEdit) - a.handleSignedIn("POST /admin/peer/edit/", a._adminPeerEditSubmit) a.handleSignedIn("GET /admin/peer/delete/", a._adminPeerDelete) a.handleSignedIn("POST /admin/peer/delete/", a._adminPeerDeleteSubmit) diff --git a/hub/templates/network/peer-delete.html b/hub/templates/network/peer-delete.html index afb5aca..0161999 100644 --- a/hub/templates/network/peer-delete.html +++ b/hub/templates/network/peer-delete.html @@ -4,7 +4,7 @@ {{with .Peer -}}
-{{- end}} -{{- end}} diff --git a/hub/templates/network/peer-view.html b/hub/templates/network/peer-view.html index 3d41b02..11bf105 100644 --- a/hub/templates/network/peer-view.html +++ b/hub/templates/network/peer-view.html @@ -1,7 +1,6 @@ {{define "body" -}}