164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|