Files
Fabian Gasper 7079f383d4 first commit
2026-07-12 18:32:01 +02:00

122 lines
3.0 KiB
Go

package main
import (
"log"
"net/http"
"os"
"strings"
"time"
"context"
"os/signal"
"syscall"
"github.com/gorilla/mux"
"golang.org/x/time/rate"
)
func noListing(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if strings.HasSuffix(req.URL.Path, "/") {
http.NotFound(w, req)
return
}
next.ServeHTTP(w, req)
})
}
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
next.ServeHTTP(w, req)
})
}
var webhookLimiter = rate.NewLimiter(rate.Every(10*time.Second), 1)
func rateLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if !webhookLimiter.Allow() {
log.Println("webhook rate limit exceeded")
w.WriteHeader(http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, req)
})
}
func wkdHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, req)
})
}
func healthHandler(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
func main() {
cfg := loadConfig()
if cfg.EnableWebhook {
cloneOrOpen(cfg)
}
contentFS := http.FileServer(http.Dir(cfg.ContentDir))
keysFS := http.FileServer(http.Dir(cfg.KeysDir))
r := mux.NewRouter()
r.Use(securityHeaders)
api := r.PathPrefix("/api/").Subrouter()
api.HandleFunc("/healthz", healthHandler).Methods("GET", "HEAD")
if cfg.EnableWebhook {
r.Handle("/webhook", rateLimit(webhookHandler(cfg))).Methods("POST")
}
// Eigener Bereich für .well-known: kein Listing, WKD-Header
wkd := r.PathPrefix("/.well-known/").Subrouter()
wkd.Use(noListing)
wkd.Use(wkdHeaders)
wkd.PathPrefix("/").Handler(keysFS)
// Alles andere: normaler Fileserver, Listing erlaubt, hier kommt später zB /downloads/ rein
r.PathPrefix("/").Handler(contentFS)
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
}
// Server in eigener Goroutine starten, damit main() weiterlaufen kann
go func() {
log.Printf("Listening on :%s, content=%s keys=%s (webhook: %v)", cfg.Port, cfg.ContentDir, cfg.KeysDir, cfg.EnableWebhook)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Auf SIGTERM/SIGINT warten
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
<-stop
log.Println("Shutdown signal received, draining connections...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("graceful shutdown failed: %v", err)
} else {
log.Println("Server shut down cleanly")
}
}