Refactor - now wireguard based. (#7)

This commit is contained in:
2026-06-12 15:11:01 +00:00
parent 5ae075647d
commit 9a3cb2d1c2
105 changed files with 3776 additions and 4251 deletions

View File

@@ -19,8 +19,10 @@ import (
var migrations embed.FS
type API struct {
db *sql.DB
lock sync.Mutex
db *sql.DB
lock sync.Mutex
sessionsMu sync.Mutex
sessions map[string]*Session
}
func New(dbPath string) (*API, error) {
@@ -34,10 +36,17 @@ func New(dbPath string) (*API, error) {
}
a := &API{
db: sqlDB,
db: sqlDB,
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 {
@@ -62,12 +71,8 @@ func (a *API) ensurePassword() error {
return db.Config_Insert(a.db, conf)
}
func (a *API) Config_Get() *Config {
conf, err := db.Config_Get(a.db, 1)
if err != nil {
panic(err)
}
return conf
func (a *API) Config_Get() (*Config, error) {
return db.Config_Get(a.db, 1)
}
func (a *API) Config_Update(conf *Config) error {
@@ -75,56 +80,78 @@ func (a *API) Config_Update(conf *Config) error {
}
func (a *API) Session_Delete(sessionID string) error {
return db.Session_Delete(a.db, sessionID)
a.sessionsMu.Lock()
defer a.sessionsMu.Unlock()
delete(a.sessions, sessionID)
return nil
}
func (a *API) Session_Get(sessionID string) (*Session, error) {
if sessionID == "" {
return a.session_CreatePub()
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()
s, ok := a.sessions[sessionID]
if sessionID == "" || !ok {
return Session{}, nil
}
session, err := db.Session_Get(a.db, sessionID)
if timeSince(s.LastSeenAt) > sessionTTLSecs {
delete(a.sessions, sessionID)
return Session{}, nil
}
s.LastSeenAt = time.Now().Unix()
return *s, nil
}
// 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 a.session_CreatePub()
return Session{}, err
}
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
return Session{}, ErrNotAuthorized
}
if timeSince(session.LastSeenAt) > 86400*21 {
return a.session_CreatePub()
}
if timeSince(session.LastSeenAt) > 86400*7 {
session.LastSeenAt = time.Now().Unix()
if err := db.Session_UpdateLastSeenAt(a.db, session.SessionID); err != nil {
log.Printf("Failed to update session: %v", err)
}
}
return session, nil
}
func (a *API) session_CreatePub() (*Session, error) {
a.sessionsMu.Lock()
defer a.sessionsMu.Unlock()
s := &Session{
SessionID: idgen.NewToken(),
CSRF: idgen.NewToken(),
SignedIn: false,
SignedIn: true,
CreatedAt: time.Now().Unix(),
LastSeenAt: time.Now().Unix(),
}
err := db.Session_Insert(a.db, s)
return s, err
a.sessions[s.SessionID] = s
return *s, nil
}
func (a *API) Session_DeleteBefore(timestamp int64) error {
return db.Session_DeleteBefore(a.db, timestamp)
}
func (a *API) Session_SignIn(s *Session, pwd string) error {
conf := a.Config_Get()
if err := bcrypt.CompareHashAndPassword(conf.Password, []byte(pwd)); err != nil {
return ErrNotAuthorized
// 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()
}
return db.Session_SetSignedIn(a.db, s.SessionID)
}
func (a *API) Network_Create(n *Network) error {
@@ -141,14 +168,13 @@ func (a *API) Network_Get(id int64) (*Network, error) {
}
func (a *API) Network_List() ([]*Network, error) {
const query = db.Network_SelectQuery + ` ORDER BY Name ASC`
const query = db.Network_SelectQuery + ` ORDER BY LocalDomain ASC`
return db.Network_List(a.db, query)
}
func (a *API) Peer_CreateNew(p *Peer) error {
p.Version = idgen.NextID(0)
p.PubKey = []byte{}
p.PubSignKey = []byte{}
p.WGPubKey = []byte{}
p.SignPubKey = []byte{}
p.APIKey = idgen.NewToken()
return db.Peer_Insert(a.db, p)
@@ -158,21 +184,22 @@ func (a *API) Peer_Init(peer *Peer, args m.PeerInitArgs) error {
a.lock.Lock()
defer a.lock.Unlock()
peer.Version = idgen.NextID(0)
peer.PubKey = args.EncPubKey
peer.PubSignKey = args.PubSignKey
// Re-read from DB inside the lock — the caller's copy was fetched before
// we held the lock, so it may be stale under concurrent requests.
current, err := db.Peer_Get(a.db, peer.NetworkID, peer.PeerIP)
if err != nil {
return err
}
if len(current.WGPubKey) != 0 {
return errors.New("peer already initialized")
}
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)
}