269 lines
6.5 KiB
Go
269 lines
6.5 KiB
Go
//go:generate go run git.rootprojects.org/root/go-gitver/v2
|
|
|
|
package main
|
|
|
|
import (
|
|
"compress/flate"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.example.com/example/goserv/assets"
|
|
"git.example.com/example/goserv/internal/api"
|
|
"git.example.com/example/goserv/internal/db"
|
|
"git.rootprojects.org/root/keypairs"
|
|
|
|
"github.com/go-chi/chi"
|
|
"github.com/go-chi/chi/middleware"
|
|
|
|
_ "github.com/joho/godotenv/autoload"
|
|
)
|
|
|
|
var (
|
|
name = "goserv"
|
|
version = "0.0.0"
|
|
date = "0001-01-01T00:00:00Z"
|
|
commit = "0000000"
|
|
)
|
|
|
|
func usage() {
|
|
fmt.Println(ver())
|
|
fmt.Println("")
|
|
fmt.Println("Use 'help <command>'")
|
|
fmt.Println(" help")
|
|
fmt.Println(" init")
|
|
fmt.Println(" run")
|
|
}
|
|
|
|
func ver() string {
|
|
return fmt.Sprintf("%s v%s %s (%s)", name, version, commit[:7], date)
|
|
}
|
|
|
|
var defaultAddr = ":3000"
|
|
|
|
type runOptions struct {
|
|
listen string
|
|
trustProxy bool
|
|
compress bool
|
|
static string
|
|
pub string
|
|
oidcWL string
|
|
demo bool
|
|
}
|
|
|
|
var runFlags *flag.FlagSet
|
|
var runOpts runOptions
|
|
var initFlags *flag.FlagSet
|
|
var dbURL string
|
|
|
|
func init() {
|
|
// chosen by fair dice roll.
|
|
// guaranteed to be random.
|
|
rand.Seed(4)
|
|
|
|
// j/k
|
|
rand.Seed(time.Now().UnixNano())
|
|
initFlags = flag.NewFlagSet("init", flag.ExitOnError)
|
|
var conftodo bool
|
|
var confdomain string
|
|
initFlags.BoolVar(&conftodo, "todo", false, "TODO init should copy out nice templated config files")
|
|
initFlags.StringVar(&confdomain, "base-url", "https://example.com", "TODO the domain to use for templated scripts")
|
|
|
|
runOpts = runOptions{}
|
|
runFlags = flag.NewFlagSet("run", flag.ExitOnError)
|
|
runFlags.StringVar(
|
|
&runOpts.listen, "listen", "",
|
|
"the address and port on which to listen (default \""+defaultAddr+"\")")
|
|
runFlags.BoolVar(&runOpts.trustProxy, "trust-proxy", false, "trust X-Forwarded-* headers")
|
|
runFlags.BoolVar(&runOpts.compress, "compress", true, "enable compression for text,html,js,css,etc")
|
|
runFlags.StringVar(&runOpts.static, "serve-path", "", "path to serve, falls back to built-in web app")
|
|
runFlags.StringVar(&runOpts.pub, "public-key", "", "path to public key, or key string - RSA or ECDSA, JWK (JSON) or PEM")
|
|
runFlags.StringVar(&runOpts.oidcWL, "oidc-whitelist", "", "list of trusted OIDC issuer URLs (ex: Auth0, Google, PocketID) for SSO")
|
|
runFlags.BoolVar(&runOpts.demo, "demo", false, "demo mode enables unauthenticated 'DELETE /api/public/reset' to reset")
|
|
runFlags.StringVar(&dbURL, "database-url", "",
|
|
"database (postgres) connection url (default postgres://postgres:postgres@localhost:5432/postgres)",
|
|
)
|
|
}
|
|
|
|
func main() {
|
|
args := os.Args[:]
|
|
if 1 == len(args) {
|
|
// "run" should be the default
|
|
args = append(args, "run")
|
|
}
|
|
|
|
if "help" == args[1] {
|
|
// top-level help
|
|
if 2 == len(args) {
|
|
usage()
|
|
os.Exit(0)
|
|
return
|
|
}
|
|
// move help to subcommand argument
|
|
self := args[0]
|
|
args = append([]string{self}, args[2:]...)
|
|
args = append(args, "--help")
|
|
}
|
|
|
|
switch args[1] {
|
|
case "version":
|
|
fmt.Println(ver())
|
|
os.Exit(0)
|
|
return
|
|
case "init":
|
|
initFlags.Parse(args[2:])
|
|
case "run":
|
|
runFlags.Parse(args[2:])
|
|
if "" == runOpts.listen {
|
|
listen := os.Getenv("LISTEN")
|
|
port := os.Getenv("PORT")
|
|
if len(listen) > 0 {
|
|
runOpts.listen = listen
|
|
} else if len(port) > 0 {
|
|
runOpts.listen = "127.0.0.1:" + port
|
|
} else {
|
|
runOpts.listen = defaultAddr
|
|
}
|
|
}
|
|
if "" == dbURL {
|
|
dbURL = os.Getenv("DATABASE_URL")
|
|
}
|
|
if "" == dbURL {
|
|
dbURL = "postgres://postgres:postgres@localhost:5432/postgres"
|
|
}
|
|
api.OIDCWhitelist = runOpts.oidcWL
|
|
if "" == api.OIDCWhitelist {
|
|
api.OIDCWhitelist = os.Getenv("OIDC_WHITELIST")
|
|
}
|
|
if "" == runOpts.pub {
|
|
runOpts.pub = os.Getenv("PUBLIC_KEY")
|
|
}
|
|
serve()
|
|
default:
|
|
usage()
|
|
os.Exit(1)
|
|
return
|
|
}
|
|
}
|
|
|
|
func initDB(connStr string) {
|
|
// TODO url.Parse
|
|
if strings.Contains(connStr, "@localhost/") || strings.Contains(connStr, "@localhost:") {
|
|
connStr += "?sslmode=disable"
|
|
} else {
|
|
connStr += "?sslmode=required"
|
|
}
|
|
|
|
err := db.Init(connStr)
|
|
if nil != err {
|
|
log.Println("db connection error", err)
|
|
//log.Fatal("db connection error", err)
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func serve() {
|
|
initDB(dbURL)
|
|
|
|
r := chi.NewRouter()
|
|
|
|
// A good base middleware stack
|
|
if runOpts.trustProxy {
|
|
api.TrustProxy = true
|
|
r.Use(middleware.RealIP)
|
|
}
|
|
if runOpts.compress {
|
|
r.Use(middleware.Compress(flate.DefaultCompression))
|
|
}
|
|
r.Use(middleware.Logger)
|
|
r.Use(middleware.Recoverer)
|
|
|
|
r.Get("/api/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(ver() + "\n"))
|
|
})
|
|
|
|
var pub keypairs.PublicKey = nil
|
|
if "" != runOpts.pub {
|
|
var err error
|
|
pub, err = keypairs.ParsePublicKey([]byte(runOpts.pub))
|
|
if nil != err {
|
|
b, err := ioutil.ReadFile(runOpts.pub)
|
|
if nil != err {
|
|
// ignore
|
|
} else {
|
|
pub, err = keypairs.ParsePublicKey(b)
|
|
if nil != err {
|
|
log.Fatal("could not parse public key:", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ = api.Init(pub, r)
|
|
|
|
var staticHandler http.HandlerFunc
|
|
pubdir := http.FileServer(assets.Assets)
|
|
|
|
if len(runOpts.static) > 0 {
|
|
// try the user-provided directory first, then fallback to the built-in
|
|
devFS := http.Dir(runOpts.static)
|
|
dev := http.FileServer(devFS)
|
|
staticHandler = func(w http.ResponseWriter, r *http.Request) {
|
|
if _, err := devFS.Open(r.URL.Path); nil != err {
|
|
pubdir.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
dev.ServeHTTP(w, r)
|
|
}
|
|
} else {
|
|
staticHandler = func(w http.ResponseWriter, r *http.Request) {
|
|
pubdir.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
if runOpts.demo {
|
|
if err := db.CanDropAllTables(dbURL); nil != err {
|
|
fmt.Fprintf(os.Stderr, err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("DANGER: running in demo mode with DELETE /api/public/reset enabled")
|
|
fmt.Fprintf(os.Stderr, "DANGER: running in demo mode with DELETE /api/public/reset enabled\n")
|
|
r.Delete("/api/public/reset", func(w http.ResponseWriter, r *http.Request) {
|
|
if err := db.DropAllTables(db.PleaseDoubleCheckTheDatabaseURLDontDropProd(dbURL)); nil != err {
|
|
w.Write([]byte("error dropping tabels: " + err.Error()))
|
|
return
|
|
}
|
|
api.Reset()
|
|
initDB(dbURL)
|
|
w.Write([]byte("re-initialized\n"))
|
|
})
|
|
}
|
|
|
|
r.Get("/*", staticHandler)
|
|
|
|
fmt.Println("")
|
|
fmt.Println("Listening for http (with reasonable timeouts) on", runOpts.listen)
|
|
srv := &http.Server{
|
|
Addr: runOpts.listen,
|
|
Handler: r,
|
|
ReadHeaderTimeout: 2 * time.Second,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 20 * time.Second,
|
|
MaxHeaderBytes: 1024 * 1024, // 1MiB
|
|
}
|
|
if err := srv.ListenAndServe(); nil != err {
|
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
|
time.Sleep(100 * time.Millisecond)
|
|
os.Exit(1)
|
|
return
|
|
}
|
|
}
|