diff options
-rw-r--r-- | api/exampleReq.go | 59 | ||||
-rw-r--r-- | api/main.go | 42 | ||||
-rw-r--r-- | api/utils.go | 29 | ||||
-rw-r--r-- | cmd/redq/main.go | 17 | ||||
-rw-r--r-- | cmd/redqctl/main.go | 74 | ||||
-rw-r--r-- | db/account.go | 112 | ||||
-rw-r--r-- | db/bearer.go | 127 | ||||
-rw-r--r-- | db/main.go | 74 | ||||
-rw-r--r-- | db/utils.go | 24 | ||||
-rw-r--r-- | flake.lock | 27 | ||||
-rw-r--r-- | flake.nix | 25 | ||||
-rw-r--r-- | go.mod | 13 | ||||
-rw-r--r-- | go.sum | 9 |
13 files changed, 632 insertions, 0 deletions
diff --git a/api/exampleReq.go b/api/exampleReq.go new file mode 100644 index 0000000..d4ec862 --- /dev/null +++ b/api/exampleReq.go @@ -0,0 +1,59 @@ +package api + +import ( + "fmt" + "log" + "net/http" + + redqdb "sinanmohd.com/redq/db" +) + +type examplApiName struct { + db *redqdb.SafeDB + req *RequestApiName + resp *ResponseApiName +} + +type RequestApiName struct { + BearerToken string +} + +type ResponseApiName struct { + Bearer *redqdb.Bearer + Error string +} + +func newExamplApiName(db *redqdb.SafeDB) *examplApiName { + a := &examplApiName{} + a.db = db + + return a +} + +func (a *examplApiName) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + a.req = &RequestApiName{} + a.resp = &ResponseApiName{} + a.resp.Bearer = &redqdb.Bearer{} + + err := unmarshal(r.Body, a.req) + if err != nil { + log.Println(err) + return + } + + err = a.resp.Bearer.VerifyAndUpdate(a.db, a.req.BearerToken) + if err != nil { + log.Println(err) + a.resp.Error = err.Error() + return + } + + json, err := marshal(a.resp) + if err != nil { + log.Println(err) + a.resp.Error = err.Error() + return + } + + fmt.Fprintf(rw, json) +} diff --git a/api/main.go b/api/main.go new file mode 100644 index 0000000..29f71c9 --- /dev/null +++ b/api/main.go @@ -0,0 +1,42 @@ +package api + +import ( + "fmt" + "net/http" + + redqdb "sinanmohd.com/redq/db" +) + +func Run(db *redqdb.SafeDB) { + const prefix string = "POST /_redq/api" + + exampleApi := newExamplApiName(db) + http.Handle(prefix+"/example", exampleApi) + + http.HandleFunc("GET /{$}", home) + http.ListenAndServe(":8008", nil) +} + +func home(rw http.ResponseWriter, r *http.Request) { + const index string = ` + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <title>🚨 redq</title> + </head> + <body> + <center> + <h1 style="font-size: 10em"> + redq is active + </h1> + <p style="font-weight: bold"> + we're soo back 🥳 + </p> + </center> + </body> + </html> + ` + + fmt.Fprint(rw, index) +} diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 0000000..40b41e6 --- /dev/null +++ b/api/utils.go @@ -0,0 +1,29 @@ +package api + +import ( + "encoding/json" + "io" +) + +func unmarshal(r io.Reader, v any) error { + body, err := io.ReadAll(r) + if err != nil { + return err + } + + err = json.Unmarshal(body, v) + if err != nil { + return err + } + + return nil +} + +func marshal(v any) (string, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + + return string(b), nil +} diff --git a/cmd/redq/main.go b/cmd/redq/main.go new file mode 100644 index 0000000..1f153d3 --- /dev/null +++ b/cmd/redq/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "log" + + redqapi "sinanmohd.com/redq/api" + redqdb "sinanmohd.com/redq/db" +) + +func main() { + db, err := redqdb.NewSafeDB() + if err != nil { + log.Fatal(err) + } + + redqapi.Run(db) +} diff --git a/cmd/redqctl/main.go b/cmd/redqctl/main.go new file mode 100644 index 0000000..f6f9e9e --- /dev/null +++ b/cmd/redqctl/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + + redqdb "sinanmohd.com/redq/db" +) + +func help() { + const helpString string = +`redqctl is a tool for managing redq. + +Usage: + + redqctl <command> [arguments] + +The commands are: + + create create a redq account + help show this help cruft + +` + + fmt.Print(helpString) +} + +func create(args []string, db *redqdb.SafeDB) { + f := flag.NewFlagSet("create", flag.ExitOnError) + ac := &redqdb.Account{} + ac.Info = &redqdb.Login{} + + f.StringVar(&ac.Email, "email", "", + "The email to associate with the account") + f.StringVar(&ac.Info.FirstName, "fname", "", + "The first name to associate with the account") + f.StringVar(&ac.Info.LastName, "lname", "", + "The last name to associate with the account") + f.StringVar(&ac.PassHash, "pass", "", + "The password to associate with the account") + f.UintVar(&ac.Info.Level, "level", 0, + "The level to associate with the account") + f.Parse(args) + + err := ac.CreateAccount(db) + if err != nil { + log.Fatal(err) + } +} + +func main() { + args := os.Args[1:] + if len(args) == 0 { + help() + os.Exit(2) + } + + db, err := redqdb.NewSafeDB() + if err != nil { + log.Fatal(err) + } + + switch args[0] { + case "help": + help() + case "create": + create(args[1:], db) + default: + help() + os.Exit(2) + } +} diff --git a/db/account.go b/db/account.go new file mode 100644 index 0000000..2c99045 --- /dev/null +++ b/db/account.go @@ -0,0 +1,112 @@ +package db + +import "errors" + +type Account struct { + Email string + PassHash string + + Info *Login +} + +type Login struct { + id uint + Level uint + FirstName, LastName string + Bearer *Bearer +} + +func (ac *Account) CreateAccount(safe *SafeDB) error { + const sqlStatement string = ` + INSERT INTO Accounts ( + id, + Email, + PassHash, + Level, + FirstName, + LastName + ) + VALUES (NULL, ?, ?, ?, ?, ?); + ` + + safe.mu.Lock() + defer safe.mu.Unlock() + + _, err := safe.db.Exec( + sqlStatement, + ac.Email, + ToBlake3(ac.PassHash), + + ac.Info.FirstName, + ac.Info.LastName, + ac.Info.Level, + ) + + return err +} + +func (ac *Account) Login(safe *SafeDB) error { + const sqlStatementQuery string = ` + SELECT id, PassHash, Level, FirstName, LastName + FROM Accounts + WHERE Accounts.Email = ? + ` + + ac.Info = &Login{} + ac.Info.Bearer = &Bearer{} + safe.mu.Lock() + row := safe.db.QueryRow(sqlStatementQuery, ac.Email) + safe.mu.Unlock() + + var PassHash string + err := row.Scan( + &ac.Info.id, + &PassHash, + &ac.Info.FirstName, + &ac.Info.LastName, + &ac.Info.Level, + ) + if err != nil { + return err + } + if PassHash != ac.PassHash { + return errors.New("Auth failed") + } + + err = ac.Info.Bearer.Generate(safe, ac.Info) + if err != nil { + return err + } + + return err +} + +func (ac *Account) fromBearer(safe *SafeDB, b *Bearer) error { + const sqlStatementAccount string = ` + SELECT Email, PassHash, Level, FirstName, LastName + FROM Accounts + WHERE Accounts.id = ? + ` + + safe.mu.Lock() + row := safe.db.QueryRow(sqlStatementAccount, b.accountId) + safe.mu.Unlock() + + ac.Info = &Login{} + ac.Info.id = b.accountId + ac.Info.Bearer = b + err := row.Scan( + &ac.Email, + &ac.PassHash, + + &ac.Info.FirstName, + &ac.Info.LastName, + &ac.Info.Level, + ) + if err != nil { + return err + } + ac.Info.Bearer = b + + return err +} diff --git a/db/bearer.go b/db/bearer.go new file mode 100644 index 0000000..b16d506 --- /dev/null +++ b/db/bearer.go @@ -0,0 +1,127 @@ +package db + +import ( + "errors" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +type Bearer struct { + id, accountId uint + Token string + ValidUpTo time.Time +} + +func (b *Bearer) FromToken(safe *SafeDB, Token string) error { + const sqlStatementBearer string = ` + SELECT id, ValidUpTo, accountId + FROM Bearer + WHERE Bearer.Token = ? + ` + + b.Token = Token + var ValidUpToString string + safe.mu.Lock() + row := safe.db.QueryRow(sqlStatementBearer, Token) + safe.mu.Unlock() + + err := row.Scan( + &b.id, + &ValidUpToString, + &b.accountId, + ) + if err != nil { + return err + } + + layout := "2006-01-02 15:04:05.999999999-07:00" + b.ValidUpTo, err = time.Parse(layout, ValidUpToString) + if err != nil { + return err + } + + timeNow := time.Now() + if timeNow.After(b.ValidUpTo) { + return errors.New("Outdated Bearer Token") + } + + return err +} + +func (b *Bearer) Update(safe *SafeDB) error { + const sqlStatementBearer string = ` + UPDATE Bearer + SET ValidUpTo = ? + WHERE id = ? + ` + + validUpTo := time.Now().Add(time.Hour * 24) + safe.mu.Lock() + _, err := safe.db.Exec(sqlStatementBearer, validUpTo, b.id) + safe.mu.Unlock() + if err != nil { + return err + } + b.ValidUpTo = validUpTo + + return nil +} + +func (b *Bearer) VerifyAndUpdate(safe *SafeDB, token string) error { + err := b.FromToken(safe, token) + if err != nil { + return err + } + + err = b.Update(safe) + if err != nil { + return err + } + + return nil +} + +func (b *Bearer) Generate(safe *SafeDB, lg *Login) error { + const sqlGenBearer string = ` + INSERT INTO Bearer ( + id, + Token, + ValidUpTo, + accountId + ) + VALUES (NULL, ?, ?, ?); + ` + + Token, err := GenRandomString(128) + if err != nil { + return err + } + + timeNow := time.Now() + ValidUpTo := timeNow.Add(time.Hour * 24) + safe.mu.Lock() + res, err := safe.db.Exec( + sqlGenBearer, + Token, + ValidUpTo, + lg.id, + ) + safe.mu.Unlock() + if err != nil { + return err + } + + id, err := res.LastInsertId() + if err != nil { + return err + } + + b.id = uint(id) + b.accountId = lg.id + b.Token = Token + b.ValidUpTo = ValidUpTo + lg.Bearer = b + + return err +} diff --git a/db/main.go b/db/main.go new file mode 100644 index 0000000..c78ea3a --- /dev/null +++ b/db/main.go @@ -0,0 +1,74 @@ +package db + +import ( + "database/sql" + "os" + "path/filepath" + "sync" + + _ "github.com/mattn/go-sqlite3" +) + +type SafeDB struct { + mu sync.Mutex + + path string + db *sql.DB +} + +func (safe *SafeDB) setupPath() error { + const path string = "/var/lib/redq/" + const name string = "redq.sqlite3" + + err := os.MkdirAll(path, os.ModeDir) + if err != nil { + return err + } + + safe.path = filepath.Join(path, name) + return nil +} + +func NewSafeDB() (*SafeDB, error) { + const create string = ` + CREATE TABLE IF NOT EXISTS Accounts( + id INTEGER PRIMARY KEY, + Email CHAR(64) NOT NULL UNIQUE, + PassHash CHAR(128) NOT NULL, + + Level INTEGER NOT NULL, + FirstName CHAR(32) NOT NULL, + LastName CHAR(32) NOT NULL + ); + + CREATE TABLE IF NOT EXISTS Bearer( + id INTEGER PRIMARY KEY, + Token CHAR(128) NOT NULL UNIQUE, + ValidUpTo TIME NOT NULL, + accountId INTEGER NOT NULL, + + FOREIGN KEY (accountId) + REFERENCES Accounts (id) + ); + ` + safe := &SafeDB{} + err := safe.setupPath() + if err != nil { + return nil, err + } + + safe.mu.Lock() + defer safe.mu.Unlock() + + safe.db, err = sql.Open("sqlite3", safe.path) + if err != nil { + return nil, err + } + + _, err = safe.db.Exec(create) + if err != nil { + return nil, err + } + + return safe, nil +} diff --git a/db/utils.go b/db/utils.go new file mode 100644 index 0000000..0b0f1cb --- /dev/null +++ b/db/utils.go @@ -0,0 +1,24 @@ +package db + +import ( + "encoding/base64" + "lukechampine.com/blake3" + "math/rand" +) + +func ToBlake3(pass string) string { + hash := blake3.Sum512([]byte(pass)) + hash64b := base64.StdEncoding.EncodeToString(hash[:]) + + return "blake3-" + hash64b +} + +func GenRandomString(n int) (string, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(b)[:n], nil +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6b7d91d --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1709479366, + "narHash": "sha256-n6F0n8UV6lnTZbYPl1A9q1BS0p4hduAv1mGAP17CVd0=", + "owner": "NixOs", + "repo": "nixpkgs", + "rev": "b8697e57f10292a6165a20f03d2f42920dfaf973", + "type": "github" + }, + "original": { + "owner": "NixOs", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..813d633 --- /dev/null +++ b/flake.nix @@ -0,0 +1,25 @@ +{ + inputs.nixpkgs.url = "github:NixOs/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: let + lib = nixpkgs.lib; + + supportedSystems = lib.platforms.unix; + forSystem = f: system: f { + inherit system; + pkgs = import nixpkgs { inherit system; }; + }; + forAllSystems = f: lib.genAttrs supportedSystems (forSystem f); + in { + devShells = forAllSystems ({ system, pkgs, ... }: { + default = pkgs.mkShell { + name = "dev"; + + buildInputs = with pkgs; [ go_1_22 gopls jq ]; + shellHook = '' + export PS1="\033[0;36m[ ]\033[0m $PS1" + ''; + }; + }); + }; +} @@ -0,0 +1,13 @@ +module sinanmohd.com/redq + +go 1.22.0 + +require ( + github.com/mattn/go-sqlite3 v1.14.22 + lukechampine.com/blake3 v1.2.1 +) + +require ( + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + golang.org/x/sys v0.8.0 // indirect +) @@ -0,0 +1,9 @@ +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= |