diff --git a/mplexer/authstore/authstore_test.go b/mplexer/authstore/authstore_test.go new file mode 100644 index 0000000..a17d790 --- /dev/null +++ b/mplexer/authstore/authstore_test.go @@ -0,0 +1,59 @@ +package authstore + +import ( + "fmt" + "testing" +) + +func TestStore(t *testing.T) { + // Note: output is cached + + store, err := NewStore(nil) + if nil != err { + t.Fatal("connection error", err) + return + } + + num := "8" + slug := num + "-xxx-client" + pubkey := num + "-somehash" + auth1 := &Authorization{ + Slug: slug, + SharedKey: "3-xxxx-zzzz-yyyy", + PublicKey: pubkey, + } + err = store.Add(auth1) + if nil != err { + t.Fatal("add error", err) + return + } + + auth, err := store.Get(slug) + if nil != err { + t.Fatal("get by slug error", err) + return + } + + auth, err = store.Get(pubkey) + if nil != err { + t.Fatal("get by pub error", err) + return + } + + auth1.MachinePPID = "a-secretish-id" + err = store.Set(auth1) + if nil != err { + t.Fatal("set machine id", err) + return + } + + err = store.Delete(auth1) + if nil != err { + t.Fatal("set machine id", err) + return + } + + store.Close() + + fmt.Printf("%#v\n", auth) +} diff --git a/mplexer/authstore/init.sql b/mplexer/authstore/init.sql new file mode 100644 index 0000000..8c7514e --- /dev/null +++ b/mplexer/authstore/init.sql @@ -0,0 +1,20 @@ +CREATE extension IF NOT EXISTS pgcrypto; +SET TIMEZONE='UTC'; + +--DROP TABLE IF EXISTS authorizations; +CREATE TABLE IF NOT EXISTS authorizations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug TEXT NOT NULL, + shared_key TEXT NOT NULL, + public_key TEXT NOT NULL, + machine_ppid TEXT NOT NULL DEFAULT '', + created_at TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'), + updated_at TIMESTAMP NOT NULL DEFAULT (now() AT TIME ZONE 'UTC'), + deleted_at TIMESTAMP NOT NULL DEFAULT ('epoch' AT TIME ZONE 'UTC') +); + +--CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_slug ON authorizations (slug); +CREATE INDEX IF NOT EXISTS idx_slug ON authorizations (slug); +CREATE INDEX IF NOT EXISTS idx_machine_ppid ON authorizations (machine_ppid); +CREATE INDEX IF NOT EXISTS idx_public_key ON authorizations (public_key); + diff --git a/mplexer/authstore/insert.sql b/mplexer/authstore/insert.sql new file mode 100644 index 0000000..5b86db6 --- /dev/null +++ b/mplexer/authstore/insert.sql @@ -0,0 +1,10 @@ +BEGIN; +LOCK TABLE authorizations IN SHARE ROW EXCLUSIVE MODE; +INSERT INTO authorizations + (slug, shared_key, public_key) +SELECT 'xxx-client-1', 'xxxx-yyyy-zzzz', 'somehash' +WHERE + NOT EXISTS ( + SELECT slug FROM authorizations WHERE slug = 'xxx-client-1' + ); +COMMIT; diff --git a/mplexer/authstore/postgresql.go b/mplexer/authstore/postgresql.go new file mode 100644 index 0000000..fb0883c --- /dev/null +++ b/mplexer/authstore/postgresql.go @@ -0,0 +1,205 @@ +package authstore + +import ( + "context" + "database/sql" + "fmt" + "io/ioutil" + "strings" + "time" + + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" +) + +type Authorization struct { + ID string `db:"id,omitempty"` + Slug string `db:"slug,omitempty"` + MachinePPID string `db:"machine_ppid,omitempty"` + PublicKey string `db:"public_key,omitempty"` + SharedKey string `db:"shared_key,omitempty"` + CreatedAt time.Time `db:"created_at,omitempty"` + UpdatedAt time.Time `db:"updated_at,omitempty"` + DeletedAt time.Time `db:"deleted_at,omitempty"` +} + +type Store interface { + Add(auth *Authorization) error + Set(auth *Authorization) error + Get(id string) (*Authorization, error) + GetBySlug(id string) (*Authorization, error) + GetByPub(id string) (*Authorization, error) + Delete(auth *Authorization) error + Close() error +} + +type StoreConfig interface { + Type() string + URL() string +} + +func NewStore(c StoreConfig) (Store, error) { + // https://godoc.org/github.com/lib/pq + + connStr := "postgres://postgres:postgres@localhost/postgres" + if strings.Contains(connStr, "@localhost/") { + connStr += "?sslmode=disable" + } else { + connStr += "?sslmode=required" + } + // TODO url.Parse + dbtype := "postgres" + sqlBytes, err := ioutil.ReadFile("./init.sql") + if nil != err { + return nil, err + } + + ctx, done := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer done() + db, err := sql.Open(dbtype, connStr) + if err := db.PingContext(ctx); nil != err { + return nil, err + } + if _, err := db.ExecContext(ctx, string(sqlBytes)); nil != err { + return nil, err + } + + dbx := sqlx.NewDb(db, dbtype) + + return &PGStore{ + dbx: dbx, + }, nil +} + +type PGStore struct { + dbx *sqlx.DB +} + +func (s *PGStore) Add(auth *Authorization) error { + ctx, done := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer done() + + tx, err := s.dbx.DB.BeginTx(ctx, &sql.TxOptions{}) + if nil != err { + return err + } + query1 := `LOCK TABLE authorizations IN SHARE ROW EXCLUSIVE MODE` + _, err = tx.ExecContext(ctx, query1) + if nil != err { + return err + } + query2 := ` + INSERT INTO authorizations (slug, shared_key, public_key) + SELECT $1, $2, $3 + WHERE NOT EXISTS ( + SELECT slug FROM authorizations WHERE deleted_at = '1970-01-01 00:00:00' AND slug = $1 + ) + ` + res, err := tx.ExecContext(ctx, query2, auth.Slug, auth.SharedKey, auth.PublicKey) + if nil != err { + return err + } + + // PostgreSQL does support RowsAffected(), but not LastInsertId() + if count, _ := res.RowsAffected(); count != 1 { + return fmt.Errorf("record not added (probably exists)") + } + + if err := tx.Commit(); nil != err { + return err + } + + return nil +} + +func (s *PGStore) Set(auth *Authorization) error { + ctx, done := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer done() + query := ` + UPDATE authorizations SET + machine_ppid=$1, + shared_key=$2, + public_key=$3 + WHERE + deleted_at = '1970-01-01 00:00:00' + AND shared_key = $2 + AND machine_ppid= '' + ` + row, err := s.dbx.ExecContext(ctx, query, auth.MachinePPID, auth.SharedKey, auth.PublicKey) + if nil != err { + return err + } + // PostgreSQL does support RowsAffected() + if count, _ := row.RowsAffected(); count != 1 { + return fmt.Errorf("record exists") + } + return nil +} + +func (s *PGStore) Get(id string) (*Authorization, error) { + ctx, done := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer done() + query := `SELECT * FROM authorizations WHERE deleted_at = '1970-01-01 00:00:00' AND (slug = $1 OR public_key = $1)` + row := s.dbx.QueryRowxContext(ctx, query, id) + if nil != row { + auth := &Authorization{} + if err := row.StructScan(auth); nil != err { + fmt.Println("what's wrong here", err) + return nil, err + } + return auth, nil + } + return nil, nil +} + +func (s *PGStore) GetBySlug(id string) (*Authorization, error) { + ctx, done := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer done() + query := `SELECT * FROM authorizations WHERE deleted_at = '1970-01-01 00:00:00' AND slug = $1` + row := s.dbx.QueryRowxContext(ctx, query, id) + if nil != row { + auth := &Authorization{} + if err := row.StructScan(auth); nil != err { + return nil, err + } + return auth, nil + } + return nil, nil +} + +func (s *PGStore) GetByPub(id string) (*Authorization, error) { + ctx, done := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer done() + query := `SELECT * FROM authorizations WHERE deleted_at = '1970-01-01 00:00:00' AND public_key = $1` + row := s.dbx.QueryRowxContext(ctx, query, id) + if nil != row { + auth := &Authorization{} + if err := row.StructScan(auth); nil != err { + return nil, err + } + return auth, nil + } + return nil, nil +} + +func (s *PGStore) Delete(auth *Authorization) error { + ctx, done := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer done() + query := ` + UPDATE authorizations SET deleted_at = 'now' + WHERE deleted_at = '1970-01-01 00:00:00' AND slug = $1 + ` + row, err := s.dbx.ExecContext(ctx, query, auth.Slug) + if nil != err { + return err + } + // PostgreSQL does support RowsAffected() + if count, _ := row.RowsAffected(); count != 1 { + return fmt.Errorf("record exists") + } + return nil +} + +func (s *PGStore) Close() error { + return s.dbx.DB.Close() +} diff --git a/mplexer/cmd/sqlstore/sqlstore.go b/mplexer/cmd/sqlstore/sqlstore.go new file mode 100644 index 0000000..8a6ef03 --- /dev/null +++ b/mplexer/cmd/sqlstore/sqlstore.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "log" + + "git.coolaj86.com/coolaj86/go-telebitd/mplexer/authstore" +) + +func main() { + store, err := authstore.NewStore(nil) + if nil != err { + log.Fatal("connection error", err) + return + } + + num := "8" + slug := num + "-xxx-client" + pubkey := num + "-somehash" + auth1 := &authstore.Authorization{ + Slug: slug, + SharedKey: "3-xxxx-zzzz-yyyy", + PublicKey: pubkey, + } + err = store.Add(auth1) + if nil != err { + log.Fatal("add error", err) + return + } + + auth, err := store.Get(slug) + if nil != err { + log.Fatal("get by slug error", err) + return + } + + auth, err = store.Get(pubkey) + if nil != err { + log.Fatal("get by pub error", err) + return + } + + auth1.MachinePPID = "a-secretish-id" + err = store.Set(auth1) + if nil != err { + log.Fatal("set machine id", err) + return + } + + err = store.Delete(auth1) + if nil != err { + log.Fatal("set machine id", err) + return + } + + store.Close() + + fmt.Printf("%#v\n", auth) +}