package main import ( "database/sql" _ "embed" "encoding/json" "errors" "flag" "fmt" "io" "net/http" "os" "strings" _ "github.com/mattn/go-sqlite3" ) //go:embed schema.sql var schemaSQL string type Config struct { Admins []string Port int Host string InstanceName string `yaml:"instance_name"` AllowAnon bool `yaml:"allow_anon"` Debug bool DBPath string `yaml:"db_path"` } type iostreams struct { Err io.Writer Out io.Writer } type Opts struct { ConfigPath string IO iostreams Log func(string) Logf func(string, ...interface{}) Config Config DB *sql.DB Reset bool } func main() { var configFlag = flag.String("config", "config.yml", "A path to a config file.") var resetFlag = flag.Bool("reset", false, "reset the database. WARNING this wipes everything.") flag.Parse() io := iostreams{ Err: os.Stderr, Out: os.Stdout, } opts := &Opts{ ConfigPath: *configFlag, Reset: *resetFlag, IO: io, // TODO use real logger Log: func(s string) { fmt.Fprintln(io.Out, s) }, Logf: func(s string, args ...interface{}) { fmt.Fprintf(io.Out, s, args...) fmt.Fprintf(io.Out, "\n") }, } err := _main(opts) if err != nil { fmt.Fprintf(os.Stderr, "failed: %s", err) } } type Teardown func() func setupDB(opts *Opts) (Teardown, error) { db, err := sql.Open("sqlite3", opts.Config.DBPath) fmt.Printf("DBG %#v\n", db) opts.DB = db return func() { db.Close() }, err } func _main(opts *Opts) error { cfg, err := parseConfig(opts.ConfigPath) if err != nil { fmt.Fprintf(os.Stderr, "could not read config file '%s'", opts.ConfigPath) os.Exit(1) } opts.Config = *cfg teardown, err := setupDB(opts) if err != nil { return fmt.Errorf("could not initialize DB: %w", err) } defer teardown() err = ensureSchema(*opts) if err != nil { return err } setupAPI(*opts) // TODO TLS or SSL or something opts.Logf("starting server at %s:%d", cfg.Host, cfg.Port) if err := http.ListenAndServe(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port), nil); err != nil { return fmt.Errorf("http server exited with error: %w", err) } return nil } func ensureSchema(opts Opts) error { db := opts.DB if opts.Reset { err := os.Remove(opts.Config.DBPath) if err != nil { return fmt.Errorf("failed to delete database: %w", err) } } rows, err := db.Query("select version from meta") if err == nil { defer rows.Close() rows.Next() var version string err = rows.Scan(&version) if err != nil { return fmt.Errorf("failed to check database schema version: %w", err) } else if version == "" { return errors.New("database is in unknown state") } return nil } if !strings.Contains(err.Error(), "no such table") { return fmt.Errorf("got error checking database state: %w", err) } _, err = db.Exec(schemaSQL) if err != nil { return fmt.Errorf("failed to initialize database schema: %w", err) } return nil } func handler(opts Opts, f http.HandlerFunc) http.HandlerFunc { // TODO make this more real return func(w http.ResponseWriter, req *http.Request) { opts.Log(req.URL.Path) f(w, req) } } // TODO I'm not entirely sold on this hash system; without transport // encryption, it doesn't really help anything. I'd rather have plaintext + // transport encryption and then, on the server side, proper salted hashing. type User struct { // TODO ID string } type BBJResponse struct { Error bool `json:"error"` Data interface{} `json:"data"` Usermap map[string]User `json:"usermap"` } func writeResponse(w http.ResponseWriter, resp BBJResponse) { json.NewEncoder(w).Encode(resp) } func setupAPI(opts Opts) { http.HandleFunc("/instance", handler(opts, func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") writeResponse(w, BBJResponse{ Data: opts.Config.InstanceName, }) })) http.HandleFunc("/check_auth", handler(opts, func(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { http.Error(w, "bad method", 400) return } type args struct { TargetUser string `json:"target_user"` TargetHash string `json:"target_hash"` } var a args err := json.NewDecoder(req.Body).Decode(&a) if err != nil { http.Error(w, "could not parse arguments", 400) } opts.Logf("got %s %s", a.TargetUser, a.TargetHash) db := opts.DB serverErr := func(err error) { opts.Logf("check_auth error: %s", err.Error()) http.Error(w, "database error", 500) } stmt, err := db.Prepare("select auth_hash from users where user_name = ?") if err != nil { serverErr(err) return } defer stmt.Close() var authHash string err = stmt.QueryRow(a.TargetUser).Scan(&authHash) if err != nil { // TODO check if there were just no results and return 404 serverErr(err) return } // TODO unique constraint on user_name if authHash != a.TargetHash { // TODO 403 probably } w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") writeResponse(w, BBJResponse{ Data: result, }) })) }