Files
plumber/cmd/server/main.go
T
codegirl007 59513ab75e Harden auth: setup secret, throttling, session destroy, secure cookies.
Replace username-based admin bootstrap with a one-time setup secret, rate-limit login/register, equalize login bcrypt timing, cap passwords at 72 bytes, destroy sessions on logout, and require Secure cookies when PORT is set.
2026-08-22 11:47:42 -07:00

127 lines
2.9 KiB
Go

package main
import (
"context"
"database/sql"
"errors"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/joho/godotenv"
"plumber"
"plumber/internal/blob"
"plumber/internal/store"
"plumber/internal/web"
)
func main() {
_ = godotenv.Load()
db, sessions := openDB()
defer db.Close()
defer sessions.Close()
uploader := blob.FromEnv()
handler := newHandler(db, sessions, uploader)
run(&http.Server{
Addr: listenAddr(),
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 90 * time.Second,
})
}
func openDB() (*sql.DB, *store.SessionStore) {
databaseURL := strings.TrimSpace(os.Getenv("DATABASE_URL"))
if databaseURL == "" {
log.Fatal("DATABASE_URL is required")
}
db, sessions, err := store.OpenPostgres(databaseURL, plumber.SchemaSQL)
if err != nil {
log.Fatalf("database: %v", err)
}
log.Printf("database: postgres")
return db, sessions
}
func newHandler(db *sql.DB, sessions *store.SessionStore, uploader blob.Uploader) http.Handler {
srv, err := web.New(store.NewPostgres(db), sessions.Store(), plumber.TemplateFS, plumber.StaticFS, web.Config{
AdminSetupSecret: strings.TrimSpace(os.Getenv("ADMIN_SETUP_SECRET")),
SecureCookie: secureCookieFromEnv(),
TrustProxy: os.Getenv("TRUST_PROXY") == "1",
Blob: uploader,
})
if err != nil {
log.Fatalf("server: %v", err)
}
return srv.Handler()
}
// secureCookieFromEnv defaults to secure when PORT is set (PaaS/production)
// and refuses an explicit disable in that environment.
func secureCookieFromEnv() bool {
v := strings.TrimSpace(os.Getenv("SECURE_COOKIE"))
if strings.TrimSpace(os.Getenv("PORT")) != "" {
if v == "0" {
log.Fatal("SECURE_COOKIE=0 is not allowed when PORT is set")
}
return true
}
return v == "1"
}
func run(httpSrv *http.Server) {
errCh := make(chan error, 1)
go func() {
log.Printf("listening on %s", httpSrv.Addr)
errCh <- httpSrv.ListenAndServe()
}()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
select {
case err := <-errCh:
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
case sig := <-sigCh:
log.Printf("shutdown signal: %v", sig)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := httpSrv.Shutdown(ctx); err != nil {
log.Printf("shutdown: %v", err)
}
if err := <-errCh; err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
}
// listenAddr prefers PORT (App Platform / PaaS), then LISTEN, then :8080.
func listenAddr() string {
if p := strings.TrimSpace(os.Getenv("PORT")); p != "" {
if strings.HasPrefix(p, ":") {
return p
}
return ":" + p
}
return env("LISTEN", ":8080")
}
func env(key, fallback string) string {
v := strings.TrimSpace(os.Getenv(key))
if v == "" {
return fallback
}
return v
}