package platform import ( "encoding/json" "errors" "fmt" "html/template" "io" "io/fs" "log/slog" "net/http" "net/url" "strings" "time" webassets "github.com/b1tsblog/license-platform/web" ) type Server struct { cfg Config store *Store vault *Vault logger *slog.Logger mux *http.ServeMux templates *template.Template loginLimiter *limiter apiLimiter *limiter } type loginData struct { Brand string CSRF string Error string } type portalData struct { Brand, PublicURL, PortalTitle, RoleLabel, Headline, Subline string User User CSRF, Message, Error string KeysReady bool Keys *KeySet TrustStoreJSON string Users, Customers, Resellers []User Licenses []LicenseRecord Audit []AuditEvent ActiveCount, ExpiringCount, RevokedCount int } type tokenData struct { Brand string License LicenseRecord Token string } func New(cfg Config, store *Store, logger *slog.Logger) (*Server, error) { if err := cfg.normalize(); err != nil { return nil, err } if store == nil { return nil, errors.New("store is required") } vault, err := NewVault(cfg.MasterKey) if err != nil { return nil, err } if logger == nil { logger = slog.Default() } funcs := template.FuncMap{ "formatTime": func(value int64) string { if value <= 0 { return "–" } return time.Unix(value, 0).UTC().Format("02.01.2006 · 15:04 UTC") }, "initial": func(value string) string { value = strings.TrimSpace(value) if value == "" { return "?" } return strings.ToUpper(string([]rune(value)[0])) }, } templates, err := template.New("portal").Funcs(funcs).ParseFS(webassets.FS, "templates/*.html") if err != nil { return nil, err } s := &Server{cfg: cfg, store: store, vault: vault, logger: logger, mux: http.NewServeMux(), templates: templates, loginLimiter: newLimiter(8, 15*time.Minute), apiLimiter: newLimiter(180, time.Minute)} if err := s.bootstrap(); err != nil { return nil, err } s.routes() return s, nil } func (s *Server) Handler() http.Handler { return s.securityHeaders(s.requestSizeLimit(s.requestLog(s.mux))) } func (s *Server) bootstrap() error { users := s.store.ListUsers() if len(users) > 0 { return nil } if strings.TrimSpace(s.cfg.BootstrapUsername) == "" || strings.TrimSpace(s.cfg.BootstrapPassword) == "" { return errors.New("bootstrap admin credentials are required for an empty data store") } hash, err := HashPassword(s.cfg.BootstrapPassword) if err != nil { return fmt.Errorf("bootstrap password: %w", err) } created, err := s.store.EnsureBootstrapAdmin(s.cfg.BootstrapUsername, s.cfg.BootstrapName, hash) if err == nil && created { s.logger.Warn("bootstrap administrator created", "username", normalizeUsername(s.cfg.BootstrapUsername)) } return err } func (s *Server) routes() { staticFS, _ := fs.Sub(webassets.FS, "static") s.mux.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(staticFS)))) s.mux.HandleFunc("GET /healthz", s.health) s.mux.HandleFunc("GET /.well-known/license-server", s.discovery) s.mux.HandleFunc("GET /api/v1/trust-store", s.publicTrustStore) s.mux.HandleFunc("GET /login", s.loginPage) s.mux.HandleFunc("POST /login", s.login) s.mux.HandleFunc("POST /logout", s.logout) s.mux.HandleFunc("POST /account/password", s.changePassword) s.mux.HandleFunc("GET /", s.portal) s.mux.HandleFunc("POST /admin/keys/generate", s.generateKeysForm) s.mux.HandleFunc("POST /admin/keys/import", s.importKeysForm) s.mux.HandleFunc("POST /admin/users", s.createUserForm) s.mux.HandleFunc("POST /reseller/customers", s.createCustomerForm) s.mux.HandleFunc("POST /licenses", s.issueForm) s.mux.HandleFunc("GET /licenses/{id}/token", s.tokenPage) s.mux.HandleFunc("POST /licenses/{id}/revoke", s.revokeForm) s.mux.HandleFunc("POST /licenses/{id}/restore", s.restoreForm) s.mux.HandleFunc("POST /api/v1/licenses/validate", s.validateAPI) s.mux.HandleFunc("POST /v1/introspect", s.validateAPI) s.mux.HandleFunc("GET /api/v1/licenses", s.listAPI) s.mux.HandleFunc("POST /api/v1/licenses", s.issueAPI) s.mux.HandleFunc("POST /api/v1/licenses/import", s.importAPI) s.mux.HandleFunc("POST /api/v1/licenses/{id}/revoke", s.revokeAPI) s.mux.HandleFunc("POST /api/v1/licenses/{id}/restore", s.restoreAPI) // Compatibility with the original reference server and licenseweb. s.mux.HandleFunc("GET /v1/admin/licenses", s.listAPI) s.mux.HandleFunc("POST /v1/admin/licenses", s.legacyRegisterAPI) s.mux.HandleFunc("POST /v1/admin/licenses/{id}/revoke", s.revokeAPI) s.mux.HandleFunc("POST /v1/admin/licenses/{id}/restore", s.restoreAPI) } func (s *Server) health(w http.ResponseWriter, _ *http.Request) { _, ready := s.store.KeySet() s.writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "keysReady": ready, "version": "1.0.0"}) } func (s *Server) discovery(w http.ResponseWriter, _ *http.Request) { s.writeJSON(w, http.StatusOK, map[string]any{"issuer": s.cfg.Brand, "serverUrl": s.cfg.PublicURL, "validationEndpoint": s.cfg.PublicURL + "/api/v1/licenses/validate", "trustStoreEndpoint": s.cfg.PublicURL + "/api/v1/trust-store"}) } func (s *Server) publicTrustStore(w http.ResponseWriter, _ *http.Request) { store, err := s.trustStore() if err != nil { s.writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": err.Error()}) return } s.writeJSON(w, http.StatusOK, store) } func (s *Server) loginPage(w http.ResponseWriter, r *http.Request) { if _, _, ok := s.currentUser(r); ok { http.Redirect(w, r, "/", http.StatusSeeOther) return } s.render(w, "login.html", loginData{Brand: s.cfg.Brand, CSRF: s.loginCSRF(w, r), Error: r.URL.Query().Get("error")}) } func (s *Server) login(w http.ResponseWriter, r *http.Request) { if !s.loginLimiter.Allow(remoteIP(r)) { http.Error(w, "too many login attempts", http.StatusTooManyRequests) return } if err := r.ParseForm(); err != nil || !s.checkLoginCSRF(r) { http.Error(w, "invalid request", http.StatusBadRequest) return } username := normalizeUsername(r.FormValue("username")) password := r.FormValue("password") if len(username) > 200 || len(password) > 1024 { http.Redirect(w, r, "/login?error=Anmeldung+fehlgeschlagen", http.StatusSeeOther) return } user, ok := s.store.FindUserByUsername(username) if !ok || !user.Active || !VerifyPassword(user.PasswordHash, password) { _ = s.audit(User{}, "login.failed", username, "invalid credentials", r) http.Redirect(w, r, "/login?error=Anmeldung+fehlgeschlagen", http.StatusSeeOther) return } raw, session, err := s.newSession(user) if err != nil { http.Error(w, "session creation failed", http.StatusInternalServerError) return } s.loginLimiter.Reset(remoteIP(r)) s.setSessionCookie(w, raw, time.Unix(session.ExpiresAt, 0)) _ = s.audit(user, "login.succeeded", user.ID, "portal session created", r) http.Redirect(w, r, "/", http.StatusSeeOther) } func (s *Server) logout(w http.ResponseWriter, r *http.Request) { user, session, ok := s.requireUser(w, r) if !ok { return } if !s.verifyCSRF(r, session) { http.Error(w, "invalid CSRF token", http.StatusForbidden) return } _ = s.store.DeleteSession(session.IDHash) s.clearSessionCookie(w) _ = s.audit(user, "logout", user.ID, "session closed", r) http.Redirect(w, r, "/login", http.StatusSeeOther) } func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) { user, session, ok := s.requireUser(w, r) if !ok { return } if !s.verifyCSRF(r, session) { http.Error(w, "invalid CSRF token", http.StatusForbidden) return } if !VerifyPassword(user.PasswordHash, r.FormValue("currentPassword")) { _ = s.audit(user, "password.change.failed", user.ID, "current password mismatch", r) http.Redirect(w, r, "/?error=Aktuelles+Passwort+ist+falsch", http.StatusSeeOther) return } hash, err := HashPassword(r.FormValue("newPassword")) if err != nil { http.Redirect(w, r, "/?error="+queryValue(err.Error()), http.StatusSeeOther) return } if err := s.store.UpdatePassword(user.ID, hash); err != nil { http.Error(w, "password update failed", http.StatusInternalServerError) return } _ = s.store.DeleteSessionsForUser(user.ID) s.clearSessionCookie(w) _ = s.audit(user, "password.changed", user.ID, "all sessions invalidated", r) http.Redirect(w, r, "/login?error=Passwort+geändert.+Bitte+neu+anmelden", http.StatusSeeOther) } func (s *Server) portal(w http.ResponseWriter, r *http.Request) { user, session, ok := s.requireUser(w, r) if !ok { return } licenses := s.store.ListLicensesFor(user) keys, keysReady := s.store.KeySet() data := portalData{Brand: s.cfg.Brand, PublicURL: s.cfg.PublicURL, User: user, CSRF: session.CSRFToken, KeysReady: keysReady, Keys: keys, Licenses: licenses, Message: r.URL.Query().Get("message"), Error: r.URL.Query().Get("error")} data.PortalTitle, data.RoleLabel, data.Headline, data.Subline = roleCopy(user.Role) for _, record := range licenses { if record.Revoked { data.RevokedCount++ } else { data.ActiveCount++ } if !record.Revoked && record.ExpiresAt <= time.Now().UTC().Add(30*24*time.Hour).Unix() { data.ExpiringCount++ } } if user.Role == RoleAdmin { data.Users = s.store.ListUsers() data.Audit = s.store.ListAudit(50) } for _, candidate := range s.store.ListUsers() { if candidate.Role == RoleReseller && candidate.Active && user.Role == RoleAdmin { data.Resellers = append(data.Resellers, candidate) } if candidate.Role == RoleCustomer && candidate.Active { if user.Role == RoleAdmin || (user.Role == RoleReseller && candidate.ParentUserID == user.ID) { data.Customers = append(data.Customers, candidate) } } } if keysReady { if trust, err := s.trustStore(); err == nil { data.TrustStoreJSON = trustStoreJSON(trust) } } s.render(w, "portal.html", data) } func roleCopy(role Role) (string, string, string, string) { switch role { case RoleAdmin: return "Admin / Management", "Management", "Steuere Vertrauen, Zugänge und Lizenzbestand.", "Zentrale Kontrolle über Signaturschlüssel, Nutzer, Aussteller und den vollständigen Audit-Trail." case RoleReseller: return "Reseller / Autor", "Reseller", "Lizenzen ausstellen, ohne die Root Keys zu sehen.", "Erstelle und verwalte ausschließlich die von dir verantworteten Kundenlizenzen." default: return "Kunde / Client", "Kunde", "Deine Lizenzen. Transparent und jederzeit verfügbar.", "Prüfe Laufzeit, Status, Features und sichere Client-Konfigurationen für deine Produkte." } } func (s *Server) generateKeysForm(w http.ResponseWriter, r *http.Request) { user, session, ok := s.requireRole(w, r, RoleAdmin) if !ok { return } if !s.verifyCSRF(r, session) { http.Error(w, "invalid CSRF token", http.StatusForbidden) return } err := s.generateKeys(r.FormValue("issuerKeyId"), r.FormValue("leaseKeyId")) if err != nil { _ = s.audit(user, "keys.generate.denied", "keyset", err.Error(), r) http.Redirect(w, r, "/?error="+queryValue(err.Error()), http.StatusSeeOther) return } _ = s.audit(user, "keys.generated", "keyset", "issuer and lease key pairs initialized", r) http.Redirect(w, r, "/?message=Schlüsselpaare+wurden+erzeugt+und+gesperrt", http.StatusSeeOther) } func (s *Server) importKeysForm(w http.ResponseWriter, r *http.Request) { user, session, ok := s.requireRole(w, r, RoleAdmin) if !ok { return } if !s.verifyCSRF(r, session) { http.Error(w, "invalid CSRF token", http.StatusForbidden) return } err := s.importKeys(r.FormValue("issuerKeyId"), r.FormValue("issuerPrivate"), r.FormValue("leaseKeyId"), r.FormValue("leasePrivate")) if err != nil { _ = s.audit(user, "keys.import.denied", "keyset", err.Error(), r) http.Redirect(w, r, "/?error="+queryValue(err.Error()), http.StatusSeeOther) return } _ = s.audit(user, "keys.imported", "keyset", "existing issuer and lease keys imported and locked", r) http.Redirect(w, r, "/?message=Bestehende+Schlüsselpaare+verschlüsselt+importiert+und+gesperrt", http.StatusSeeOther) } func (s *Server) createUserForm(w http.ResponseWriter, r *http.Request) { actor, session, ok := s.requireRole(w, r, RoleAdmin) if !ok { return } if !s.verifyCSRF(r, session) { http.Error(w, "invalid CSRF token", http.StatusForbidden) return } role := Role(strings.TrimSpace(r.FormValue("role"))) if role != RoleAdmin && role != RoleReseller && role != RoleCustomer { http.Redirect(w, r, "/?error=Ungültige+Rolle", http.StatusSeeOther) return } hash, err := HashPassword(r.FormValue("password")) if err != nil { http.Redirect(w, r, "/?error="+queryValue(err.Error()), http.StatusSeeOther) return } parentUserID := "" if role == RoleCustomer { parentUserID = strings.TrimSpace(r.FormValue("resellerUserId")) if parentUserID != "" { parent, ok := s.store.GetUser(parentUserID) if !ok || parent.Role != RoleReseller || !parent.Active { http.Redirect(w, r, "/?error=Zugeordneter+Reseller+ist+ungültig", http.StatusSeeOther) return } } } user := User{ID: newID("usr"), Username: r.FormValue("username"), DisplayName: strings.TrimSpace(r.FormValue("displayName")), Role: role, ParentUserID: parentUserID, PasswordHash: hash, Active: true} if user.DisplayName == "" { user.DisplayName = user.Username } if err := s.store.CreateUser(user); err != nil { http.Redirect(w, r, "/?error="+queryValue(err.Error()), http.StatusSeeOther) return } _ = s.audit(actor, "user.created", user.ID, string(role)+":"+user.Username, r) http.Redirect(w, r, "/?message=Benutzer+angelegt", http.StatusSeeOther) } func (s *Server) createCustomerForm(w http.ResponseWriter, r *http.Request) { actor, session, ok := s.requireRole(w, r, RoleReseller) if !ok { return } if !s.verifyCSRF(r, session) { http.Error(w, "invalid CSRF token", http.StatusForbidden) return } hash, err := HashPassword(r.FormValue("password")) if err != nil { http.Redirect(w, r, "/?error="+queryValue(err.Error()), http.StatusSeeOther) return } user := User{ID: newID("usr"), Username: r.FormValue("username"), DisplayName: strings.TrimSpace(r.FormValue("displayName")), Role: RoleCustomer, ParentUserID: actor.ID, PasswordHash: hash, Active: true} if user.DisplayName == "" { user.DisplayName = user.Username } if err := s.store.CreateUser(user); err != nil { http.Redirect(w, r, "/?error="+queryValue(err.Error()), http.StatusSeeOther) return } _ = s.audit(actor, "customer.created", user.ID, user.Username, r) http.Redirect(w, r, "/?message=Kundenportal+angelegt", http.StatusSeeOther) } func (s *Server) issueForm(w http.ResponseWriter, r *http.Request) { actor, session, ok := s.requireRole(w, r, RoleAdmin, RoleReseller) if !ok { return } if !s.verifyCSRF(r, session) { http.Error(w, "invalid CSRF token", http.StatusForbidden) return } input, err := parseIssueForm(r.Form) if err == nil { _, _, err = s.issueLicense(actor, input) } if err != nil { http.Redirect(w, r, "/?error="+queryValue(err.Error()), http.StatusSeeOther) return } _ = s.audit(actor, "license.issued", input.Product, input.Customer+" / "+input.Edition, r) http.Redirect(w, r, "/?message=Lizenz+signiert+und+registriert", http.StatusSeeOther) } func (s *Server) tokenPage(w http.ResponseWriter, r *http.Request) { user, _, ok := s.requireUser(w, r) if !ok { return } record, ok := s.store.GetLicense(r.PathValue("id")) if !ok || !canAccessLicense(user, record) { http.Error(w, "not found", http.StatusNotFound) return } token, err := s.tokenFor(record) if err != nil { http.Error(w, "token unavailable", http.StatusInternalServerError) return } s.render(w, "token.html", tokenData{Brand: s.cfg.Brand, License: record, Token: token}) } func (s *Server) revokeForm(w http.ResponseWriter, r *http.Request) { s.mutateLicenseForm(w, r, true) } func (s *Server) restoreForm(w http.ResponseWriter, r *http.Request) { s.mutateLicenseForm(w, r, false) } func (s *Server) mutateLicenseForm(w http.ResponseWriter, r *http.Request, revoked bool) { actor, session, ok := s.requireRole(w, r, RoleAdmin, RoleReseller) if !ok { return } if !s.verifyCSRF(r, session) { http.Error(w, "invalid CSRF token", http.StatusForbidden) return } record, ok := s.store.GetLicense(r.PathValue("id")) if !ok || !canManageLicense(actor, record) { http.Error(w, "not found", http.StatusNotFound) return } reason := "manually revoked" if !revoked { reason = "" } if err := s.store.SetRevoked(record.LicenseID, revoked, reason); err != nil { http.Error(w, "update failed", http.StatusInternalServerError) return } action := "license.restored" if revoked { action = "license.revoked" } _ = s.audit(actor, action, record.LicenseID, reason, r) http.Redirect(w, r, "/?message=Lizenzstatus+aktualisiert", http.StatusSeeOther) } func (s *Server) validateAPI(w http.ResponseWriter, r *http.Request) { if !s.apiLimiter.Allow(remoteIP(r)) { s.writeJSON(w, http.StatusTooManyRequests, validateResponse{Reason: "rate limit exceeded"}) return } var request validateRequest if err := decodeJSON(r, &request); err != nil { s.writeJSON(w, http.StatusBadRequest, validateResponse{Reason: err.Error()}) return } response, err := s.validateLicense(request) if err != nil { s.writeJSON(w, http.StatusForbidden, response) return } s.writeJSON(w, http.StatusOK, response) } func (s *Server) listAPI(w http.ResponseWriter, r *http.Request) { actor, err := s.apiActor(r) if err != nil { s.apiUnauthorized(w) return } records := s.store.ListLicensesFor(actor) views := make([]LicenseView, 0, len(records)) for _, record := range records { views = append(views, licenseView(record, "")) } s.writeJSON(w, http.StatusOK, map[string]any{"licenses": views}) } func (s *Server) issueAPI(w http.ResponseWriter, r *http.Request) { actor, err := s.apiActor(r) if err != nil { s.apiUnauthorized(w) return } var input IssueInput if err := decodeJSON(r, &input); err != nil { s.writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } record, token, err := s.issueLicense(actor, input) if err != nil { s.writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } _ = s.audit(actor, "license.issued.api", record.LicenseID, record.Product, r) s.writeJSON(w, http.StatusCreated, licenseView(record, token)) } func (s *Server) importAPI(w http.ResponseWriter, r *http.Request) { actor, err := s.apiActor(r) if err != nil { s.apiUnauthorized(w) return } var payload struct { Token string `json:"token"` CustomerUserID string `json:"customerUserId,omitempty"` } if err := decodeJSON(r, &payload); err != nil { s.writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } record, err := s.importLicense(actor, payload.Token, strings.TrimSpace(payload.CustomerUserID)) if err != nil { s.writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } _ = s.audit(actor, "license.imported.api", record.LicenseID, record.Product, r) s.writeJSON(w, http.StatusCreated, licenseView(record, "")) } func (s *Server) legacyRegisterAPI(w http.ResponseWriter, r *http.Request) { actor, err := s.apiActor(r) if err != nil { s.apiUnauthorized(w) return } var payload struct { Token string `json:"token"` } if err := decodeJSON(r, &payload); err != nil { s.writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } record, err := s.importLicense(actor, payload.Token, "") if err != nil { s.writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } _ = s.audit(actor, "license.registered.legacy", record.LicenseID, record.Product, r) s.writeJSON(w, http.StatusCreated, licenseView(record, "")) } func (s *Server) revokeAPI(w http.ResponseWriter, r *http.Request) { s.mutateLicenseAPI(w, r, true) } func (s *Server) restoreAPI(w http.ResponseWriter, r *http.Request) { s.mutateLicenseAPI(w, r, false) } func (s *Server) mutateLicenseAPI(w http.ResponseWriter, r *http.Request, revoked bool) { actor, err := s.apiActor(r) if err != nil { s.apiUnauthorized(w) return } record, ok := s.store.GetLicense(r.PathValue("id")) if !ok || !canManageLicense(actor, record) { s.writeJSON(w, http.StatusNotFound, map[string]string{"error": "license not found"}) return } reason := "" if revoked { var payload struct { Reason string `json:"reason"` } _ = decodeJSONAllowEmpty(r, &payload) reason = strings.TrimSpace(payload.Reason) if reason == "" { reason = "revoked via API" } } if err := s.store.SetRevoked(record.LicenseID, revoked, reason); err != nil { s.writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } action := "license.restored.api" if revoked { action = "license.revoked.api" } _ = s.audit(actor, action, record.LicenseID, reason, r) s.writeJSON(w, http.StatusOK, map[string]any{"licenseId": record.LicenseID, "revoked": revoked}) } func canAccessLicense(user User, record LicenseRecord) bool { return user.Role == RoleAdmin || (user.Role == RoleReseller && record.IssuedByUserID == user.ID) || (user.Role == RoleCustomer && record.CustomerUserID == user.ID) } func canManageLicense(user User, record LicenseRecord) bool { return user.Role == RoleAdmin || (user.Role == RoleReseller && record.IssuedByUserID == user.ID) } func (s *Server) audit(actor User, action, target, detail string, r *http.Request) error { event := AuditEvent{ID: newID("evt"), ActorID: actor.ID, Action: action, Target: target, Detail: detail, RemoteIP: remoteIP(r), CreatedAt: unixNow()} return s.store.AddAudit(event) } func (s *Server) render(w http.ResponseWriter, name string, data any) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := s.templates.ExecuteTemplate(w, name, data); err != nil { s.logger.Error("render template", "name", name, "error", err) } } func (s *Server) writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) } func (s *Server) apiUnauthorized(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", "Bearer") s.writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"}) } func decodeJSON(r *http.Request, target any) error { dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20)) dec.DisallowUnknownFields() if err := dec.Decode(target); err != nil { return err } if dec.Decode(&struct{}{}) != io.EOF { return errors.New("request body must contain one JSON object") } return nil } func decodeJSONAllowEmpty(r *http.Request, target any) error { err := decodeJSON(r, target) if errors.Is(err, io.EOF) { return nil } return err } func queryValue(value string) string { return url.QueryEscape(value) } func (s *Server) requestSizeLimit(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Body != nil { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) } next.ServeHTTP(w, r) }) } func (s *Server) securityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self'; img-src 'self' data:; script-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'") w.Header().Set("Cache-Control", "no-store") if s.cfg.SecureCookies { w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") } next.ServeHTTP(w, r) }) } func (s *Server) requestLog(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { started := time.Now() next.ServeHTTP(w, r) s.logger.Info("http request", "method", r.Method, "path", r.URL.Path, "remote_ip", remoteIP(r), "duration_ms", time.Since(started).Milliseconds()) }) }