add session stuff

This commit is contained in:
Branden J Brown 2024-01-31 22:29:31 -06:00
parent 6e63aba2b4
commit d61c70da86
3 changed files with 155 additions and 8 deletions

View File

@ -17,7 +17,7 @@ import (
const ( const (
// Argon2id parameters recommended in // Argon2id parameters recommended in
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
time = 3 timeCost = 3
memory = 12 << 10 memory = 12 << 10
threads = 1 threads = 1
hashLen = 48 hashLen = 48
@ -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()) slog.ErrorContext(ctx, "failed to get salt for new user", "user", user, "err", err.Error())
panic(err) 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() id := uuid.New()
_, err := db.Exec(ctx, `INSERT INTO shotgun_users(user, pass, salt, id) VALUES (?, ?, ?, ?)`, user, h, salt, id) _, err := db.Exec(ctx, `INSERT INTO shotgun_users(user, pass, salt, id) VALUES (?, ?, ?, ?)`, user, h, salt, id)
if err != nil { 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) 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) { if !bytes.Equal(p, h) {
slog.ErrorContext(ctx, "login failed", "user", user) slog.ErrorContext(ctx, "login failed", "user", user)
return ID{}, fmt.Errorf("invalid credentials") return ID{}, fmt.Errorf("invalid credentials")
@ -92,5 +92,5 @@ const initUsers = `CREATE TABLE shotgun_users (
user TEXT PRIMARY KEY NOT NULL, user TEXT PRIMARY KEY NOT NULL,
pass BLOB NOT NULL, pass BLOB NOT NULL,
salt BLOB NOT NULL, salt BLOB NOT NULL,
id TEXT NOT NULL id TEXT NOT NULL UNIQUE
);` ) STRICT;`

72
player/session.go Normal file
View File

@ -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;`

75
player/session_test.go Normal file
View File

@ -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)
}
}