Browse Source

Add authentication for new entries

master
NGnius 4 years ago
parent
commit
f7fa6a353b
6 changed files with 269 additions and 18 deletions
  1. +1
    -0
      config.go
  2. +58
    -0
      handlers.go
  3. +22
    -2
      json_structs.go
  4. +1
    -0
      main.go
  5. +59
    -16
      sql_service.go
  6. +128
    -0
      sql_structs.go

+ 1
- 0
config.go View File

@@ -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() {


+ 58
- 0
handlers.go View File

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


+ 22
- 2
json_structs.go View File

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

+ 1
- 0
main.go View File

@@ -31,6 +31,7 @@ func init() {
serverMux.HandleFunc("/board", boardHandler)
serverMux.HandleFunc("/player", playerHandler)
serverMux.HandleFunc("/record", newEntryHandler)
serverMux.HandleFunc("/token", newKeyHandler)
handler = serverMux
}



+ 59
- 16
sql_service.go View File

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

+ 128
- 0
sql_structs.go View File

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