From 5c6da34d9858a5181090b6462830bd9a37eccea4 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Sun, 8 Mar 2020 17:05:15 +0000 Subject: [PATCH] storing player bodies in the database --- server/db/bodies.go | 43 +++++++++++++++++++ server/db/players.go | 52 +++++++++++++++++++++++ server/db/sqlite.go | 99 +++++++++++++++++++------------------------- server/main.go | 37 +++++++++++------ server/request.go | 45 +++++++++++++++----- 5 files changed, 197 insertions(+), 79 deletions(-) create mode 100644 server/db/bodies.go diff --git a/server/db/bodies.go b/server/db/bodies.go new file mode 100644 index 0000000..3a4be62 --- /dev/null +++ b/server/db/bodies.go @@ -0,0 +1,43 @@ +package db + +import ( + "fmt" + "time" +) + +type Body struct { + ID int + PlayerID int + X float64 + Y float64 + Z float64 + DiedAt time.Time + FoundAt *time.Time +} + +func (db *SQLite) ListBodies() ([]Body, error) { + rows, err := db.db.Query(`select id, player, x, y, z, died_at from bodies where found_at is null`) + if err != nil { + return nil, fmt.Errorf("failed to fetch bodies from db: %w", err) + } + + bodies := make([]Body, 0, 64) + for rows.Next() { + var body Body + if err := rows.Scan(&body.ID, &body.PlayerID, &body.X, &body.Y, &body.Z, &body.DiedAt); err != nil { + return nil, fmt.Errorf("failed to read bodies response rows: %w", err) + } + bodies = append(bodies, body) + } + return bodies, nil +} + +func (db *SQLite) AddBody(body *Body) error { + _, err := db.db.Exec(`insert into bodies + (player, x, y, z) + values (?, ?, ?, ?);`, body.PlayerID, body.X, body.Y, body.Z) + if err != nil { + return fmt.Errorf("failed to add body to db: %w", err) + } + return nil +} diff --git a/server/db/players.go b/server/db/players.go index 831dfec..1678e39 100644 --- a/server/db/players.go +++ b/server/db/players.go @@ -1,6 +1,58 @@ package db +import ( + "crypto/rand" + "fmt" + "os" + + "golang.org/x/crypto/bcrypt" +) + +func cryptostring(n int) string { + b := make([]byte, n) + rand.Read(b) + letters := `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%*-=+` + r := make([]byte, 0, n) + for _, n := range b { + r = append(r, letters[int(n)%len(letters)]) + } + return string(r) +} + type Players interface { CreatePlayer(name, pass string) error CheckPassword(name, pass string) error } + +type Player struct { + ID int `json:"id"` + Name string `json:"name"` + Hash string `json:"-"` + Salt string `json:"-"` +} + +func (p *Player) SetPassword(password string) error { + p.Salt = cryptostring(12) + cat := []byte(password + p.Salt) + hashBytes, err := bcrypt.GenerateFromPassword(cat, 13) + if err != nil { + return fmt.Errorf("unable to generate password hash: %w", err) + } + p.Hash = string(hashBytes) + return nil +} + +func (p *Player) HasPassword(password string) bool { + cat := []byte(password + p.Salt) + if err := bcrypt.CompareHashAndPassword([]byte(p.Hash), cat); err != nil { + if err != bcrypt.ErrMismatchedHashAndPassword { + fmt.Fprintf(os.Stderr, "error checking password for %v: %v\n", p, err) + } + return false + } + return true +} + +func (p Player) String() string { + return fmt.Sprintf(`Player{ID: %d, Name: "%s"}`, p.ID, p.Name) +} diff --git a/server/db/sqlite.go b/server/db/sqlite.go index 2abbeac..f5c3ce5 100644 --- a/server/db/sqlite.go +++ b/server/db/sqlite.go @@ -6,7 +6,6 @@ import ( "os" _ "github.com/mattn/go-sqlite3" - "golang.org/x/crypto/bcrypt" ) type SQLite struct { @@ -16,7 +15,10 @@ type SQLite struct { func OpenSQLite(path string) (*SQLite, error) { db, err := sql.Open("sqlite3", path) if err != nil { - return nil, fmt.Errorf("unable to open sqlite3 database at %s: %v", path, err) + return nil, fmt.Errorf("unable to open sqlite3 database at %s: %w", path, err) + } + if _, err := db.Exec(`pragma foreign_keys = on;`); err != nil { + fmt.Fprintf(os.Stderr, "failed to enforce foreign key constraints: %v\n", err) } if _, err := db.Exec(` @@ -30,80 +32,63 @@ func OpenSQLite(path string) (*SQLite, error) { } if _, err := db.Exec(` - create table if not exists deaths ( + create table if not exists bodies ( id integer primary key autoincrement, - player integer, - died_at datetime default current_timestamp, - x real, - y real, - z real, - foreign key (player) references players(id) + player integer not null, + x real not null, + y real not null, + z real not null, + died_at datetime default current_timestamp not null, + found_at datetime, + found_by integer, + foreign key (player) references players(id), + foreign key (found_by) references players(id) );`); err != nil { - fmt.Fprintf(os.Stderr, "failed to create deaths table: %v\n", err) + fmt.Fprintf(os.Stderr, "failed to create bodies table: %v\n", err) } return &SQLite{db: db}, nil } -func (db *SQLite) CreatePlayer(name, pass, salt string) error { - combined := []byte(pass + salt) - hashBytes, err := bcrypt.GenerateFromPassword(combined, 13) - if err != nil { - return fmt.Errorf("unable to generate password hash: %v", err) - } - hash := string(hashBytes) +func (db *SQLite) CreatePlayer(p *Player) error { if _, err := db.db.Exec(` insert into players (name, phash, psalt) values (?, ?, ?); - `, name, hash, salt); err != nil { - return fmt.Errorf("unable to insert user: %v", err) + `, p.Name, p.Hash, p.Salt); err != nil { + return fmt.Errorf("unable to insert user: %w", err) } - return nil -} -func (db *SQLite) CheckPassword(name, pass string) error { - rows, err := db.db.Query(` - select phash, psalt from players where name = ?; - `, name) - if err != nil { - return fmt.Errorf("failed to fetch row for user %s: %v", name, err) + row := db.db.QueryRow(`select id from players where name = ?`, p.Name) + if err := row.Scan(&p.ID); err != nil { + return fmt.Errorf("unable to scan user ID: %w", err) } - defer rows.Close() - scannedRows := 0 - for rows.Next() { - var ( - dbhash string - dbsalt string - ) - if err := rows.Scan(&dbhash, &dbsalt); err != nil { - return fmt.Errorf("failed to scan row: %v", err) - } - scannedRows++ - if err := bcrypt.CompareHashAndPassword([]byte(dbhash), []byte(pass+dbsalt)); err != nil { - return fmt.Errorf("failed hash match: %v", err) - } + return nil +} + +func (db *SQLite) ReadPlayer(p *Player) error { + args := make([]interface{}, 0, 1) + q := `select id, name, phash, psalt from players ` + if p.ID != 0 { + q += `where id = ?` + args = append(args, p.ID) + } else { + q += `where name = ?` + args = append(args, p.Name) } - if scannedRows == 0 { - return fmt.Errorf("no such user") + row := db.db.QueryRow(q, args...) + if err := row.Scan(&p.ID, &p.Name, &p.Hash, &p.Salt); err != nil { + return fmt.Errorf("unable to read player row: %w", err) } - + fmt.Printf("read player from db: %v\n", p) return nil } -func (db *SQLite) SetPassword(name, pass, salt string) error { - combined := []byte(pass + salt) - hashBytes, err := bcrypt.GenerateFromPassword(combined, 13) - if err != nil { - return fmt.Errorf("unable to generate password hash: %v", err) - } - hash := string(hashBytes) - if _, err := db.db.Exec(` - update players - set phash = ?, psalt = ? - where name = ?; - `, hash, salt, name); err != nil { - return fmt.Errorf("unable to update user: %v", err) +func (db *SQLite) UpdatePlayer(p *Player) error { + q := `update players set name = ?, phash = ?, pstalt = ? where id = ?` + args := []interface{}{p.Name, p.Hash, p.Salt, p.ID} + if _, err := db.db.Exec(q, args...); err != nil { + return fmt.Errorf("unable to update player %s: %w", *p, err) } return nil } diff --git a/server/main.go b/server/main.go index 2234d19..5ccd1ab 100644 --- a/server/main.go +++ b/server/main.go @@ -61,21 +61,21 @@ func runPlayerCreate(cmd *cobra.Command, args []string) { } defer conn.Close() - player := args[0] + player := db.Player{Name: args[0]} var pass string if len(args) > 1 { pass = args[1] } else { pass = cryptostring(12) } - salt := cryptostring(12) + player.SetPassword(pass) - if err := conn.CreatePlayer(player, pass, salt); err != nil { + if err := conn.CreatePlayer(&player); err != nil { fmt.Fprintf(os.Stderr, "failed to create player: %v\n", err) return } - fmt.Printf("created:\n\tplayer:\t%s\n\tpass:\t%s\n", player, pass) + fmt.Printf("created:\n\tid:\t%d\n\tplayer:\t%s\n\tpass:\t%s\n", player.ID, player.Name, pass) } func runPlayerCheckPassword(cmd *cobra.Command, args []string) { @@ -85,10 +85,15 @@ func runPlayerCheckPassword(cmd *cobra.Command, args []string) { } defer conn.Close() - player := args[0] + player := db.Player{Name: args[0]} + if err := conn.ReadPlayer(&player); err != nil { + fmt.Fprintf(os.Stderr, "unable to fetch player row: %v\n", err) + return + } + pass := args[1] - if err := conn.CheckPassword(player, pass); err != nil { - fmt.Fprintf(os.Stderr, "failed password check: %v\n", err) + if !player.HasPassword(pass) { + fmt.Fprintf(os.Stderr, "bad password\n", err) } } @@ -99,11 +104,19 @@ func runPlayerSetPassword(cmd *cobra.Command, args []string) { } defer conn.Close() - player := args[0] - pass := args[1] - salt := cryptostring(12) - if err := conn.SetPassword(player, pass, salt); err != nil { - fmt.Fprintf(os.Stderr, "failed to set password: %v\n", err) + player := db.Player{Name: args[0]} + if err := conn.ReadPlayer(&player); err != nil { + fmt.Fprintf(os.Stderr, "unable to read player record: %v\n", err) + return + } + + if err := player.SetPassword(args[1]); err != nil { + fmt.Fprintf(os.Stderr, "unable to set player password: %v\n", err) + return + } + + if err := conn.UpdatePlayer(&player); err != nil { + fmt.Fprintf(os.Stderr, "unable to save player changes: %v\n", err) } } diff --git a/server/request.go b/server/request.go index 7a8839d..239c0cd 100644 --- a/server/request.go +++ b/server/request.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "time" + + "github.com/jordanorelli/kloam/db" ) type request struct { @@ -75,17 +77,29 @@ type loginResult struct { } func (l *login) exec(s *server, from *player) { - res := loginResult{} - if err := s.db.CheckPassword(l.Username, l.Password); err != nil { - res.Error = err.Error() - } else { - res.Passed = true - from.username = l.Username + sendResult := func(res loginResult) { + b, _ := json.Marshal(res) + msg := fmt.Sprintf("login-result %s", string(b)) + from.outbox <- msg + } + row := db.Player{Name: l.Username} + if err := s.db.ReadPlayer(&row); err != nil { + sendResult(loginResult{ + Error: fmt.Sprintf("failed to read player from database: %v", err), + }) + return + } + fmt.Printf("login read row from database: %v\n", row) + + if row.HasPassword(l.Password) { + sendResult(loginResult{Passed: true}) + from.username = l.Username + from.id = row.ID + } else { + sendResult(loginResult{Error: "bad password"}) + return } - b, _ := json.Marshal(res) - msg := fmt.Sprintf("login-result %s", string(b)) - from.outbox <- msg messages := make([]string, 0, len(s.souls)) @@ -105,7 +119,6 @@ func (l *login) exec(s *server, from *player) { } } }() - } type death struct { @@ -114,6 +127,18 @@ type death struct { func (d *death) exec(s *server, from *player) { s.Info("executing a death: %#v", d) + + body := &db.Body{ + PlayerID: from.id, + X: d.Position.X, + Y: d.Position.Y, + Z: d.Position.Z, + } + s.Info("adding body: %#v", body) + if err := s.db.AddBody(body); err != nil { + s.Error("db error: %v", err) + } + _soul := soul{ PlayerName: from.username, Position: d.Position,