package main import ( "log" "net/http" "os" "strconv" "time" "greencoast/internal/api" "greencoast/internal/index" "greencoast/internal/storage" ) func getenvBool(key string, def bool) bool { v := os.Getenv(key) if v == "" { return def } b, err := strconv.ParseBool(v) if err != nil { return def } return b } func staticHeaders(next http.Handler) http.Handler { onion := os.Getenv("GC_ONION_LOCATION") // optional: e.g., http://xxxxxxxx.onion/ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Security headers + strict CSP (no inline) + COEP w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") w.Header().Set("Cross-Origin-Resource-Policy", "same-site") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=()") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Strict-Transport-Security", "max-age=15552000; includeSubDomains; preload") w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp") // Allow only self + HTTPS for fetch/SSE; no inline styles/scripts w.Header().Set("Content-Security-Policy", "default-src 'self'; "+ "script-src 'self'; "+ "style-src 'self'; "+ "img-src 'self' data:; "+ "connect-src 'self' https:; "+ "frame-ancestors 'none'; object-src 'none'; base-uri 'none'; form-action 'self'; "+ "require-trusted-types-for 'script'") if onion != "" { w.Header().Set("Onion-Location", onion) } // Basic CORS for static (GET only effectively) w.Header().Set("Access-Control-Allow-Origin", "*") if r.Method == http.MethodOptions { w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func main() { // ---- Config ---- httpAddr := os.Getenv("GC_HTTP_ADDR") if httpAddr == "" { httpAddr = ":9080" } httpsAddr := os.Getenv("GC_HTTPS_ADDR") certFile := os.Getenv("GC_TLS_CERT") keyFile := os.Getenv("GC_TLS_KEY") staticAddr := os.Getenv("GC_STATIC_ADDR") if staticAddr == "" { staticAddr = ":9082" } staticDir := os.Getenv("GC_STATIC_DIR") if staticDir == "" { staticDir = "/opt/greencoast/client" } dataDir := os.Getenv("GC_DATA_DIR") if dataDir == "" { dataDir = "/var/lib/greencoast" } coarseTS := getenvBool("GC_COARSE_TS", true) // safer default (less precise metadata) zeroTrust := getenvBool("GC_ZERO_TRUST", true) encRequired := getenvBool("GC_ENCRYPTION_REQUIRED", true) // operator-blind by default requirePOP := getenvBool("GC_REQUIRE_POP", true) // logged only here signingSecretHex := os.Getenv("GC_SIGNING_SECRET_HEX") if len(signingSecretHex) < 64 { log.Printf("WARN: GC_SIGNING_SECRET_HEX length=%d (need >=64 hex chars)", len(signingSecretHex)) } else { log.Printf("GC_SIGNING_SECRET_HEX OK (len=%d)", len(signingSecretHex)) } discID := os.Getenv("GC_DISCORD_CLIENT_ID") discSecret := os.Getenv("GC_DISCORD_CLIENT_SECRET") discRedirect := os.Getenv("GC_DISCORD_REDIRECT_URI") // ---- Storage & Index ---- store, err := storage.NewFS(dataDir) if err != nil { log.Fatalf("storage init: %v", err) } ix := index.New() // Reindex on boot from existing files (coarse time if enabled) if err := store.Walk(func(hash string, size int64, mod time.Time) error { when := mod.UTC() if coarseTS { when = when.Truncate(time.Minute) } return ix.Put(index.Entry{ Hash: hash, Bytes: size, StoredAt: when.Format(time.RFC3339Nano), Private: false, // unknown here }) }); err != nil { log.Printf("reindex on boot: %v", err) } // ---- Auth providers ---- providers := api.AuthProviders{ SigningSecretHex: signingSecretHex, Discord: api.DiscordProvider{ Enabled: discID != "" && discSecret != "" && discRedirect != "", ClientID: discID, ClientSecret: discSecret, RedirectURI: discRedirect, }, } // ---- API server ---- srv := api.New(store, ix, coarseTS, zeroTrust, providers, encRequired) // ---- Static file server (separate listener) ---- go func() { fs := http.FileServer(http.Dir(staticDir)) h := staticHeaders(fs) log.Printf("static listening on %s (dir=%s)", staticAddr, staticDir) if err := http.ListenAndServe(staticAddr, h); err != nil { log.Fatalf("static server: %v", err) } }() // ---- Start API (HTTP or HTTPS) ---- if httpsAddr != "" && certFile != "" && keyFile != "" { log.Printf("API HTTPS %s POP:%v ENC_REQUIRED:%v", httpsAddr, requirePOP, encRequired) if err := srv.ListenHTTPS(httpsAddr, certFile, keyFile); err != nil { log.Fatal(err) } return } log.Printf("API HTTP %s POP:%v ENC_REQUIRED:%v", httpAddr, requirePOP, encRequired) if err := srv.ListenHTTP(httpAddr); err != nil { log.Fatal(err) } }