This repository has been archived on 2025-08-23. You can view files and clone it, but cannot push or open issues or pull requests.
Files
GreenCoast/cmd/shard/main.go
2025-08-22 18:24:24 -04:00

142 lines
4.0 KiB
Go

// cmd/shard/main.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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Security headers
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")
// 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", false)
zeroTrust := getenvBool("GC_ZERO_TRUST", true)
encRequired := getenvBool("GC_ENCRYPTION_REQUIRED", true) // operator-blind by default
requirePOP := getenvBool("GC_REQUIRE_POP", true) // for logging only; API defaults to true internally
signingSecretHex := os.Getenv("GC_SIGNING_SECRET_HEX")
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 whatever files exist on disk
if err := store.Walk(func(hash string, size int64, mod time.Time) error {
return ix.Put(index.Entry{
Hash: hash,
Bytes: size,
StoredAt: mod.UTC().Format(time.RFC3339Nano),
Private: false, // unknown here; safe default
})
}); 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)
}
}