|
|
|
@ -4,12 +4,15 @@ import ( |
|
|
|
"log" |
|
|
|
"net/http" |
|
|
|
"os" |
|
|
|
"strings" |
|
|
|
"time" |
|
|
|
|
|
|
|
root "marmic/servicetrade-toolbox" |
|
|
|
"marmic/servicetrade-toolbox/internal/handlers/web" |
|
|
|
"marmic/servicetrade-toolbox/internal/middleware" |
|
|
|
|
|
|
|
"github.com/gorilla/csrf" |
|
|
|
"github.com/gorilla/handlers" |
|
|
|
"github.com/gorilla/mux" |
|
|
|
) |
|
|
|
|
|
|
|
@ -20,6 +23,12 @@ func main() { |
|
|
|
} |
|
|
|
r := mux.NewRouter() |
|
|
|
|
|
|
|
// Make app proxy-aware (X-Forwarded-Proto/Host) so downstream middlewares see correct scheme/host
|
|
|
|
r.Use(handlers.ProxyHeaders) |
|
|
|
|
|
|
|
// Global security headers middleware
|
|
|
|
r.Use(middleware.SecurityHeaders) |
|
|
|
|
|
|
|
// Serve embedded static files
|
|
|
|
staticFS, err := root.GetStaticFS() |
|
|
|
if err != nil { |
|
|
|
@ -30,15 +39,84 @@ func main() { |
|
|
|
// Serve static files
|
|
|
|
// r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
|
|
|
|
|
|
|
// Auth routes
|
|
|
|
// Auth routes (login remains outside CSRF)
|
|
|
|
r.HandleFunc("/login", web.LoginHandler).Methods("GET", "POST") |
|
|
|
r.HandleFunc("/logout", web.LogoutHandler).Methods("GET", "POST") |
|
|
|
|
|
|
|
// CSRF protection for state-changing routes
|
|
|
|
csrfKey := os.Getenv("CSRF_AUTH_KEY") |
|
|
|
if len(csrfKey) < 32 { |
|
|
|
log.Println("WARNING: CSRF_AUTH_KEY is not set or too short; using insecure default for development only") |
|
|
|
csrfKey = "this-is-a-very-insecure-dev-key-please-set-env" |
|
|
|
} |
|
|
|
secureCookies := os.Getenv("APP_SECURE_COOKIES") != "false" |
|
|
|
disableCSRF := os.Getenv("CSRF_DISABLE") == "true" |
|
|
|
// Trusted origins for reverse-proxy/HTTPS setups (comma-separated env)
|
|
|
|
var trustedOrigins []string |
|
|
|
if envTO := os.Getenv("CSRF_TRUSTED_ORIGINS"); envTO != "" { |
|
|
|
for _, part := range strings.Split(envTO, ",") { |
|
|
|
o := strings.TrimSpace(part) |
|
|
|
if o != "" { |
|
|
|
trustedOrigins = append(trustedOrigins, o) |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Sensible defaults for your known hosts; override via CSRF_TRUSTED_ORIGINS in prod
|
|
|
|
trustedOrigins = []string{ |
|
|
|
"https://dev.toolbox.nicpatterson.info", |
|
|
|
"https://toolbox.nicpatterson.info", |
|
|
|
"http://localhost:8080", |
|
|
|
"http://127.0.0.1:8080", |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
csrfOpts := []csrf.Option{ |
|
|
|
csrf.Secure(secureCookies), |
|
|
|
csrf.SameSite(csrf.SameSiteLaxMode), |
|
|
|
csrf.CookieName("XSRF-TOKEN"), |
|
|
|
csrf.HttpOnly(false), // allow htmx script to read and send the token
|
|
|
|
csrf.Path("/"), |
|
|
|
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
|
|
// Log the specific CSRF failure reason to help debugging
|
|
|
|
origin := r.Header.Get("Origin") |
|
|
|
referer := r.Header.Get("Referer") |
|
|
|
scheme := "http" |
|
|
|
if r.TLS != nil { |
|
|
|
scheme = "https" |
|
|
|
} |
|
|
|
if xfProto := r.Header.Get("X-Forwarded-Proto"); xfProto != "" { |
|
|
|
scheme = xfProto |
|
|
|
} |
|
|
|
if reason := csrf.FailureReason(r); reason != nil { |
|
|
|
log.Printf("CSRF validation failed: %v | host=%s scheme=%s origin=%s referer=%s", reason, r.Host, scheme, origin, referer) |
|
|
|
} else { |
|
|
|
log.Printf("CSRF validation failed: unknown reason | host=%s scheme=%s origin=%s referer=%s", r.Host, scheme, origin, referer) |
|
|
|
} |
|
|
|
http.Error(w, "Forbidden - CSRF", http.StatusForbidden) |
|
|
|
})), |
|
|
|
} |
|
|
|
if len(trustedOrigins) > 0 { |
|
|
|
log.Printf("CSRF trusted origins: %v", trustedOrigins) |
|
|
|
csrfOpts = append(csrfOpts, csrf.TrustedOrigins(trustedOrigins)) |
|
|
|
} |
|
|
|
csrfMw := csrf.Protect([]byte(csrfKey), csrfOpts...) |
|
|
|
|
|
|
|
// Protected routes
|
|
|
|
protected := r.PathPrefix("/").Subrouter() |
|
|
|
protected.Use(middleware.AuthMiddleware) |
|
|
|
if disableCSRF { |
|
|
|
log.Println("WARNING: CSRF is DISABLED by environment (CSRF_DISABLE=true) - DO NOT USE IN PRODUCTION") |
|
|
|
} else { |
|
|
|
// Use standard gorilla/csrf; if origin checks are problematic in certain environments,
|
|
|
|
// fallback to simple double-submit cookie middleware via CSRF_MODE=simple
|
|
|
|
if os.Getenv("CSRF_MODE") == "simple" { |
|
|
|
protected.Use(middleware.CSRFSimple) |
|
|
|
} else { |
|
|
|
protected.Use(csrfMw) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected.HandleFunc("/", web.DashboardHandler).Methods("GET") |
|
|
|
protected.HandleFunc("/logout", web.LogoutHandler).Methods("POST") |
|
|
|
protected.HandleFunc("/jobs", web.JobsHandler).Methods("GET", "POST") |
|
|
|
protected.HandleFunc("/invoices", web.InvoicesHandler).Methods("GET", "POST") |
|
|
|
protected.HandleFunc("/ok-invoice/{id}", web.UpdateInvoiceStatusHandler).Methods("PUT") |
|
|
|
|