From f7fa6a353b8f8b62ec04f04f0acf87788e22eb88 Mon Sep 17 00:00:00 2001 From: NGnius Date: Fri, 21 Feb 2020 21:30:02 -0500 Subject: [PATCH] Add authentication for new entries --- config.go | 1 + handlers.go | 58 ++++++++++++++++++++++ json_structs.go | 24 ++++++++- main.go | 1 + sql_service.go | 75 ++++++++++++++++++++++------ sql_structs.go | 128 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 269 insertions(+), 18 deletions(-) diff --git a/config.go b/config.go index a4819ce..22a6b0e 100644 --- a/config.go +++ b/config.go @@ -25,6 +25,7 @@ func initArgs() { flag.StringVar(&sqlServer, "sql", sqlServerDefault, "SQL Database type") flag.BoolVar(&buildTables, "build-db", false, "Build database tables on startup") flag.BoolVar(&populateTables, "populate-db", false, "Populate database with test data") + flag.BoolVar(&randomizeTokens, "random-tokens", false, "Generate tokens with some random bytes") } func parseArgs() { diff --git a/handlers.go b/handlers.go index 98d4455..a7f51ed 100644 --- a/handlers.go +++ b/handlers.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "strconv" + "strings" ) func boardHandler(w http.ResponseWriter, r *http.Request) { @@ -136,6 +137,22 @@ func newEntryHandler(w http.ResponseWriter, r *http.Request) { errorResponse(405, "Non-POST method not allowed at this endpoint", w, r) return } + // check for API token + tokenHeader := r.Header.Get("Authorization") + tokenSplit := strings.Split(tokenHeader, " ") + if tokenHeader == "" || len(tokenSplit) != 2 || tokenSplit[1] == "" { + errorResponse(401, "Missing or invalid authorization header", w, r) + return + } + key, keyErr := keyByToken(tokenSplit[1]) + if keyErr != nil || key.ID == 0 { + errorResponse(401, "Invalid token", w, r) + return + } + if !key.IsEnabled() { + errorResponse(403, "Invalid token", w, r) + return + } data, readErr := ioutil.ReadAll(r.Body) if readErr != nil { fmt.Println(readErr) @@ -149,6 +166,10 @@ func newEntryHandler(w http.ResponseWriter, r *http.Request) { errorResponse(400, "Unable to convert request to JSON: "+jsonErr.Error(), w, r) return } + if newEntry.PlayerID != key.Player && !key.IsMultiuser() { + errorResponse(403, "Invalid authorization for player", w, r) + return + } sqlErr := newEntrySql(newEntry.Score, newEntry.PlayerID, newEntry.BoardID) if sqlErr != nil { fmt.Println(sqlErr) @@ -160,6 +181,43 @@ func newEntryHandler(w http.ResponseWriter, r *http.Request) { errorResponse(200, "New entry created", w, r) } +func newKeyHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + w.Header().Add("Access-Control-Allow-Origin", "*") + if r.Method != "POST" { + errorResponse(405, "Non-POST method not allowed at this endpoint", w, r) + return + } + data, readErr := ioutil.ReadAll(r.Body) + if readErr != nil { + fmt.Println(readErr) + errorResponse(500, "Unable to read HTTP request body: "+readErr.Error(), w, r) + return + } + result := NewResult("", r.URL.String()) + newKey, jsonErr := UnmarshalNewKeyJSON(data) + if jsonErr != nil { + errorResponse(400, "Unable to convert request to JSON: "+jsonErr.Error(), w, r) + return + } + key, sqlErr := newKeySql(newKey.PlayerID) + if sqlErr != nil { + fmt.Println(sqlErr) + errorResponse(500, "Key could not be created: "+sqlErr.Error(), w, r) + return + } + jsonObj, _ := key.JsonObject() + result.Items = []interface{}{jsonObj} + result.Query = fmt.Sprintf("new key[player id: %d]", newKey.PlayerID) + result.Complete() + data, err := json.Marshal(result) + if err != nil { + errorResponse(500, "Unable convert result to JSON: "+err.Error(), w, r) + return + } + w.Write(data) +} + func exampleHandler(w http.ResponseWriter, r *http.Request) { // useless function please ignore } diff --git a/json_structs.go b/json_structs.go index f416441..38ba87a 100644 --- a/json_structs.go +++ b/json_structs.go @@ -46,6 +46,26 @@ func UnmarshalNewEntryJSON(data []byte) (NewEntryJSON, error) { return neJson, nil } +// KeyJSON an API key for making new entry requests +type KeyJSON struct { + Token string + PlayerID int64 +} + +// NewKeyJSON a new API key to be generated +type NewKeyJSON struct { + PlayerID int64 +} + +func UnmarshalNewKeyJSON(data []byte) (NewKeyJSON, error) { + var nkJson NewKeyJSON + jsonErr := json.Unmarshal(data, &nkJson) + if jsonErr != nil { + return nkJson, jsonErr + } + return nkJson, nil +} + // PlayerJSON a player type PlayerJSON struct { ID int64 @@ -69,7 +89,7 @@ type ErrorJSON struct { type Result struct { StatusCode int Items []interface{} - Elapsed int64 // query time (ms) + Elapsed int64 // query time (ns) Count int Query string URL string @@ -88,5 +108,5 @@ func NewResult(q string, url string) (r Result) { // Complete finish the result func (r *Result) Complete() { r.Count = len(r.Items) - r.Elapsed = time.Since(r.Start).Milliseconds() + r.Elapsed = time.Since(r.Start).Nanoseconds() } diff --git a/main.go b/main.go index 24e4afd..9929d0b 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ func init() { serverMux.HandleFunc("/board", boardHandler) serverMux.HandleFunc("/player", playerHandler) serverMux.HandleFunc("/record", newEntryHandler) + serverMux.HandleFunc("/token", newKeyHandler) handler = serverMux } diff --git a/sql_service.go b/sql_service.go index 530874c..2780794 100644 --- a/sql_service.go +++ b/sql_service.go @@ -55,17 +55,17 @@ func sqlClose() error { func boardByName(name string) (*Board, error) { b := &Board{} - return b, db.QueryRow("SELECT * FROM Boards WHERE name=? LIMIT 1", name).Scan(b.Intake()...) + return b, db.QueryRow("SELECT * FROM Boards WHERE name=? LIMIT 1;", name).Scan(b.Intake()...) } func playerByName(name string) (*Player, error) { p := &Player{} - return p, db.QueryRow("SELECT * FROM Players WHERE name=? LIMTI 1", name).Scan(p.Intake()...) + return p, db.QueryRow("SELECT * FROM Players WHERE name=? LIMTI 1;", name).Scan(p.Intake()...) } func newEntrySql(score, player, board int64) error { tx, _ := db.Begin() - stmt, _ := tx.Prepare("INSERT INTO Entries(score, player, board, time) VALUES (?, ?, ?, ?)") + stmt, _ := tx.Prepare("INSERT INTO Entries(score, player, board, time) VALUES (?, ?, ?, ?);") _, err := stmt.Exec(score, player, board, time.Now().Unix()) if err != nil { tx.Rollback() @@ -75,6 +75,29 @@ func newEntrySql(score, player, board int64) error { return nil } +func newKeySql(player int64) (*Key, error) { + tx, _ := db.Begin() + newKey := &Key{Token: "new", Player: player, Time: time.Now().Unix()} + stmt, _ := tx.Prepare("INSERT INTO Keys(token, player, time) VALUES (?, ?, ?);") + _, err := stmt.Exec(newKey.Token, newKey.Player, newKey.Time) + if err != nil { + tx.Rollback() + return nil, err + } + tx.Commit() + db.QueryRow("SELECT * FROM Keys WHERE token=? AND player=? AND time=?", newKey.Token, newKey.Player, newKey.Time).Scan(newKey.Intake()...) + tokenErr := newKey.GenerateToken() + if tokenErr != nil { + return nil, tokenErr + } + return newKey, newKey.Commit() +} + +func keyByToken(token string) (*Key, error) { + k := &Key{} + return k, db.QueryRow("SELECT * FROM Keys WHERE token=? LIMIT 1;", token).Scan(k.Intake()...) +} + // internal operations func sqlBuildTables() { transaction, txErr := db.Begin() @@ -87,7 +110,8 @@ func sqlBuildTables() { // build real tables transaction.Exec("CREATE TABLE IF NOT EXISTS Players (id INTEGER PRIMARY KEY, name TEXT NOT NULL);") transaction.Exec("CREATE TABLE IF NOT EXISTS Boards (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL);") - transaction.Exec("CREATE TABLE IF NOT EXISTS Entries (id INTEGER PRIMARY KEY, rank INTEGER NOT NULL DEFAULT -1, score INTEGER NOT NULL, player INTEGER NOT NULL, board INTEGER NOT NULL, time INTEGER NOT NULL, metadata BLOB NOT NULL DEFAULT 0, FOREIGN KEY(player) REFERENCES Players(id), FOREIGN KEY(board) REFERENCES Boards(id));") + transaction.Exec("CREATE TABLE IF NOT EXISTS Entries (id INTEGER PRIMARY KEY, rank INTEGER NOT NULL DEFAULT -1, score INTEGER NOT NULL, player INTEGER NOT NULL, board INTEGER NOT NULL, time INTEGER NOT NULL, metadata BLOB NOT NULL DEFAULT x'FF', FOREIGN KEY(player) REFERENCES Players(id), FOREIGN KEY(board) REFERENCES Boards(id));") + transaction.Exec("CREATE TABLE IF NOT EXISTS Keys (id INTEGER PRIMARY KEY, token TEXT NOT NULL, player INTEGER NOT NULL, time INTEGER NOT NULL, metadata BLOB NOT NULL DEFAULT x'FF', FOREIGN KEY(player) REFERENCES Players(id));") transaction.Commit() } @@ -125,19 +149,20 @@ func sqlPopulateTables() { } } now := time.Now().Unix() + metadata := []byte{0x00, 0x00, 0x00, 0x00} entries := []*Entry{ - &Entry{ID: 1, Rank: 1, Score: 1000, Player: players[0].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 2, Rank: 2, Score: 900, Player: players[1].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 3, Rank: 3, Score: 400, Player: players[2].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 4, Rank: 4, Score: 350, Player: players[3].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 5, Rank: 5, Score: 350, Player: players[4].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 6, Rank: 6, Score: 250, Player: players[5].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 7, Rank: 7, Score: 200, Player: players[6].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 8, Rank: 8, Score: 175, Player: players[7].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 9, Rank: 9, Score: 150, Player: players[8].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 10, Rank: 10, Score: 140, Player: players[9].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 11, Rank: 11, Score: 10, Player: players[10].ID, Board: boards[0].ID, Time: now}, - &Entry{ID: 12, Rank: 12, Score: 60, Player: players[11].ID, Board: boards[0].ID, Time: now}, + &Entry{ID: 1, Rank: 1, Score: 1000, Player: players[0].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 2, Rank: 2, Score: 900, Player: players[1].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 3, Rank: 3, Score: 400, Player: players[2].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 4, Rank: 4, Score: 350, Player: players[3].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 5, Rank: 5, Score: 350, Player: players[4].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 6, Rank: 6, Score: 250, Player: players[5].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 7, Rank: 7, Score: 200, Player: players[6].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 8, Rank: 8, Score: 175, Player: players[7].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 9, Rank: 9, Score: 150, Player: players[8].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 10, Rank: 10, Score: 140, Player: players[9].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 11, Rank: 11, Score: 10, Player: players[10].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, + &Entry{ID: 12, Rank: 12, Score: 60, Player: players[11].ID, Board: boards[0].ID, Time: now, Metadata: metadata}, &Entry{ID: 13, Rank: 13, Score: 13, Player: players[12].ID, Board: boards[0].ID, Time: now}, } for _, e := range entries { @@ -146,4 +171,22 @@ func sqlPopulateTables() { fmt.Printf("Error creating entry %d: %s\n", e.ID, err) } } + metadata = []byte{0x7E} + keys := []*Key{ + &Key{ID: 1, Token: "", Player: players[0].ID, Time: now, Metadata: metadata}, + &Key{ID: 2, Token: "", Player: players[1].ID, Time: now, Metadata: metadata}, + } + for _, k := range keys { + genErr := k.GenerateToken() + if genErr != nil { + fmt.Printf("Error generating token for key %d, %s\n", k.ID, genErr) + } + //fmt.Printf("Key %d token: %s\n", k.ID, k.Token) + err := k.Commit() + if err != nil { + fmt.Printf("Error creating key %d, %s\n", k.ID, err) + } + // bitwise op test + //fmt.Printf("Key &d is superuser:%t, enabled:%t\n", k.ID, k.IsSuperuser(), k.IsEnabled()) + } } diff --git a/sql_structs.go b/sql_structs.go index d7108ab..28a2a98 100644 --- a/sql_structs.go +++ b/sql_structs.go @@ -5,10 +5,18 @@ package main import ( "encoding/json" "strconv" + "crypto/sha512" + "crypto/rand" + "encoding/binary" + "math/big" // test //"fmt" ) +var ( + randomizeTokens bool +) + type Jsonable interface { Json() ([]byte, error) JsonPretty() ([]byte, error) @@ -306,3 +314,123 @@ func (e *Entry) Intake() []interface{} { func (e *Entry) Output() []interface{} { return []interface{}{e.ID, e.Rank, e.Score, e.Player, e.Board, e.Time, e.Metadata} } + +type Key struct { + ID int64 + Token string + Player int64 + Time int64 // Created time (seconds since Unix epoch) + Metadata []byte +} + +func LoadKey(id int64) *Key { + k := &Key{ID: id} + loadErr := k.Load() + if loadErr != nil { + return nil + } + return k +} + +func (k *Key) Load() error { + return db.QueryRow("SELECT * FROM Keys WHERE id=? LIMIT 1;", k.ID).Scan(k.Intake()...) +} + +func (k *Key) Commit() error { + tx, _ := db.Begin() + statement, _ := tx.Prepare("INSERT OR REPLACE INTO Keys(id, token, player, time, metadata) VALUES (?, ?, ?, ?, ?);") + _, err := statement.Exec(k.Output()...) + if err != nil { + tx.Rollback() + return err + } + return tx.Commit() +} + +func (k *Key) IsEnabled() bool { + return (k.Metadata[0] & 0b00000001) == 0 +} + +func (k *Key) Disable() { + k.Metadata[0] = k.Metadata[0] | 0b00000001 +} + +func (k *Key) IsSuperuser() bool { + return (k.Metadata[0] & 0b00000100) == 0 +} + +func (k *Key) Promote() { + k.Metadata[0] = k.Metadata[0] | 0b00000100 +} + +func (k *Key) IsMultiuser() bool { + return (k.Metadata[0] & 0b00010000) == 0 +} + +func (k *Key) Develop() { + k.Metadata[0] = k.Metadata[0] | 0b00010000 +} + +func (k *Key) GenerateToken() error { + buf_int64 := make([]byte, 8) // 8 bytes = 64 bits + input := []byte{} + if randomizeTokens { + max := big.NewInt(2^16-1) + for i := 0; i < 512; i++ { + // generate randomness + num, _ := rand.Int(rand.Reader, max) + input = append(input, num.Bytes()...) + } + } + binary.PutVarint(buf_int64, k.ID) + input = append(input, buf_int64...) + binary.PutVarint(buf_int64, k.Player) + input = append(input, buf_int64...) + binary.PutVarint(buf_int64, k.Time) + input = append(input, buf_int64...) + bToken := sha512.Sum512(input) + //k.Token = string(bToken) + k.Token = "" + for _, b := range bToken { + tmp_b := b & 0b01111111 // Valid 7-bit ASCII values only + for !((tmp_b > 47 && tmp_b < 58) || (tmp_b > 64 && tmp_b < 91) || (tmp_b > 96 && tmp_b < 123)) { + tmp_b = sha512.Sum512([]byte{tmp_b})[1] + tmp_b = tmp_b & 0b01111111 + } + k.Token += string(tmp_b) + } + return nil +} + +// Implementation of Jsonable +func (k *Key) Json() ([]byte, error) { + var data []byte + jsonObj, err := k.JsonObject() + if err != nil { + return data, err + } + return json.Marshal(jsonObj) +} + +func (k *Key) JsonPretty() ([]byte, error) { + var data []byte + jsonObj, err := k.JsonObject() + if err != nil { + return data, err + } + return json.MarshalIndent(jsonObj, "", " ") +} + +func (k *Key) JsonObject() (interface{}, error) { + jsonObj := KeyJSON{Token: k.Token, PlayerID: k.Player} + return jsonObj, nil +} + +// Implementation of Rower +func (k *Key) Intake() []interface{} { + return []interface{}{&k.ID, &k.Token, &k.Player, &k.Time, &k.Metadata} +} + +func (k *Key) Output() []interface{} { + return []interface{}{k.ID, k.Token, k.Player, k.Time, k.Metadata} +}