From d61c70da867a8ed9e45f23503839cda0fc518b35 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Wed, 31 Jan 2024 22:29:31 -0600 Subject: [PATCH] add session stuff --- player/auth.go | 16 ++++----- player/session.go | 72 ++++++++++++++++++++++++++++++++++++++++ player/session_test.go | 75 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 player/session.go create mode 100644 player/session_test.go diff --git a/player/auth.go b/player/auth.go index be542b1..6f1874d 100644 --- a/player/auth.go +++ b/player/auth.go @@ -17,10 +17,10 @@ import ( const ( // Argon2id parameters recommended in // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id - time = 3 - memory = 12 << 10 - threads = 1 - hashLen = 48 + timeCost = 3 + memory = 12 << 10 + threads = 1 + hashLen = 48 ) type ( @@ -51,7 +51,7 @@ func Register(ctx context.Context, db Execer, user, pass string) error { slog.ErrorContext(ctx, "failed to get salt for new user", "user", user, "err", err.Error()) panic(err) } - h := argon2.IDKey([]byte(pass), salt, time, memory, threads, hashLen) + h := argon2.IDKey([]byte(pass), salt, timeCost, memory, threads, hashLen) id := uuid.New() _, err := db.Exec(ctx, `INSERT INTO shotgun_users(user, pass, salt, id) VALUES (?, ?, ?, ?)`, user, h, salt, id) if err != nil { @@ -80,7 +80,7 @@ func Login(ctx context.Context, db RowQuerier, user, pass string) (ID, error) { } return ID{}, fmt.Errorf("couldn't get user creds: %w", err) } - h := argon2.IDKey([]byte(pass), salt, time, memory, threads, hashLen) + h := argon2.IDKey([]byte(pass), salt, timeCost, memory, threads, hashLen) if !bytes.Equal(p, h) { slog.ErrorContext(ctx, "login failed", "user", user) return ID{}, fmt.Errorf("invalid credentials") @@ -92,5 +92,5 @@ const initUsers = `CREATE TABLE shotgun_users ( user TEXT PRIMARY KEY NOT NULL, pass BLOB NOT NULL, salt BLOB NOT NULL, - id TEXT NOT NULL -);` + id TEXT NOT NULL UNIQUE +) STRICT;` diff --git a/player/session.go b/player/session.go new file mode 100644 index 0000000..b371a4a --- /dev/null +++ b/player/session.go @@ -0,0 +1,72 @@ +package player + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/google/uuid" + "gitlab.com/zephyrtronium/sq" +) + +// Session is a session ID. +type Session = uuid.UUID + +// InitSessions initializes an SQLite table relating player IDs to sessions. +func InitSessions(ctx context.Context, db Execer) error { + _, err := db.Exec(ctx, initSessions) + if err != nil { + return fmt.Errorf("couldn't init sessions table: %w", err) + } + return nil +} + +// StartSession creates a new 24-hour session for a player as of now. +func StartSession(ctx context.Context, db Execer, p ID, now time.Time) (Session, error) { + id := uuid.New() + ttl := now.Add(24 * time.Hour).Unix() + _, err := db.Exec(ctx, `INSERT OR REPLACE INTO shotgun_session(player, id, ttl) VALUES (?, ?, ?)`, p, id, ttl) + if err != nil { + slog.ErrorContext(ctx, "couldn't start session", "player", p, "error", err.Error()) + return Session{}, fmt.Errorf("couldn't start session: %w", err) + } + slog.InfoContext(ctx, "session started", "player", p, "session", id, "now", now) + return Session(id), nil +} + +// FromSession gets the player ID associated with a session and updates the +// session to last 24 hours past now. +func FromSession(ctx context.Context, db RowQuerier, id Session, now time.Time) (ID, error) { + ttl := now.Add(24 * time.Hour).Unix() + var p ID + err := db.QueryRow(ctx, `UPDATE shotgun_session SET ttl = ? WHERE id = ? AND ttl >= ? RETURNING player;`, ttl, id, now.Unix()).Scan(&p) + switch err { + case nil: // do nothing + case sq.ErrNoRows: + slog.WarnContext(ctx, "looked for missing session", "session", id, "now", now) + return ID{}, fmt.Errorf("no such session") + default: + slog.ErrorContext(ctx, "couldn't get player from session", "session", id, "now", now, "error", err.Error()) + return ID{}, fmt.Errorf("couldn't get player from session: %w", err) + } + slog.InfoContext(ctx, "got player from session", "session", id, "player", p) + return p, nil +} + +// Logout deletes a session. +func Logout(ctx context.Context, db Execer, id Session) error { + _, err := db.Exec(ctx, `DELETE FROM shotgun_session WHERE id = ?`, id) + if err != nil { + slog.ErrorContext(ctx, "couldn't logout", "session", id, "error", err.Error()) + return fmt.Errorf("couldn't logout: %w", err) + } + slog.InfoContext(ctx, "logged out", "session", id) + return nil +} + +const initSessions = `CREATE TABLE shotgun_session ( + id TEXT PRIMARY KEY, + player TEXT NOT NULL UNIQUE, + ttl INTEGER +) STRICT;` diff --git a/player/session_test.go b/player/session_test.go new file mode 100644 index 0000000..0853cf8 --- /dev/null +++ b/player/session_test.go @@ -0,0 +1,75 @@ +package player_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "gitlab.com/zephyrtronium/sq" + + "git.sunturtle.xyz/studio/shotgun/player" + + _ "modernc.org/sqlite" // sqlite driver +) + +func TestSessions(t *testing.T) { + ctx := context.Background() + db, err := sq.Open("sqlite", ":memory:") + if err != nil { + t.Fatal(err) + } + conn, err := db.Conn(ctx) + if err != nil { + t.Fatal(err) + } + if err := player.InitSessions(ctx, conn); err != nil { + t.Fatalf("couldn't init sessions: %v", err) + } + + p := player.ID{1} + now := time.Unix(0, 0) + id, err := player.StartSession(ctx, conn, p, now) + if err != nil { + t.Fatalf("couldn't start session: %v", err) + } + if id == uuid.Nil { + t.Errorf("got zero session id at start") + } + + u, err := player.FromSession(ctx, conn, id, now.Add(25*time.Hour)) + if err == nil { + t.Errorf("no error on expired session") + } + if u != uuid.Nil { + t.Errorf("got nonzero player %v from expired session", u) + } + + u, err = player.FromSession(ctx, conn, id, now.Add(23*time.Hour)) + if err != nil { + t.Errorf("couldn't get player from session: %v", err) + } + if u == uuid.Nil { + t.Errorf("got zero player from session") + } + + u, err = player.FromSession(ctx, conn, id, now.Add(25*time.Hour)) + if err != nil { + t.Errorf("couldn't get player from extended session: %v", err) + } + if u == uuid.Nil { + t.Errorf("got zero player from extended session") + } + + if err := player.Logout(ctx, conn, id); err != nil { + t.Errorf("couldn't logout: %v", err) + } + + u, err = player.FromSession(ctx, conn, id, now.Add(25*time.Hour)) + if err == nil { + t.Errorf("no error on logged out session") + } + if u != uuid.Nil { + t.Errorf("got nonzero player %v from logged out session", u) + } +}