// Package httpx holds small HTTP helpers shared by gsg's non-page // surfaces. The admin and share handlers must make the same origin, // scheme, header and remote-address decisions; keeping them here means a // security fix lands in one place instead of three. package httpx import ( "net" "net/http" "net/url" "strings" ) // SecureOptions controls the admin/share response policy. These pages are // deliberately not the themed site: no inline script or style, so the CSP // can stay tight. type SecureOptions struct { // ScriptSrc is the CSP script-src value. Empty means "'none'"; the // notices admin page needs "'self'" for admin.js, while share pages // ship no JavaScript at all. ScriptSrc string // NoStore marks a response whose body must never be stored, even by // the requesting browser -- a minted share URL is a bearer capability. NoStore bool } // Secure sets the response headers every admin/share page must carry. func Secure(w http.ResponseWriter, opts SecureOptions) { scriptSrc := opts.ScriptSrc if scriptSrc == "" { scriptSrc = "'none'" } hdr := w.Header() hdr.Set("X-Content-Type-Options", "nosniff") hdr.Set("X-Frame-Options", "DENY") hdr.Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; script-src "+scriptSrc+ "; img-src 'self'; form-action 'self'; base-uri 'none'") if opts.NoStore { hdr.Set("Cache-Control", "no-store") } } // SameOrigin blocks a cross-site POST even if a session cookie would // otherwise be sent. A valid cookie proves the browser is logged in; it // does not prove the request came from our own page. func SameOrigin(r *http.Request) bool { switch r.Header.Get("Sec-Fetch-Site") { case "", "same-origin", "none": default: return false } if o := r.Header.Get("Origin"); o != "" { u, err := url.Parse(o) if err != nil || !strings.EqualFold(u.Host, r.Host) { return false } } return true } // IsHTTPS reports whether the browser's original request was HTTPS, // accounting for the reverse proxy's X-Forwarded-Proto. func IsHTTPS(r *http.Request) bool { return r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") } // AbsoluteURL builds an absolute URL for a path on this host, using the // same proxy-aware scheme as IsHTTPS. func AbsoluteURL(r *http.Request, path string) string { scheme := "http" if IsHTTPS(r) { scheme = "https" } return scheme + "://" + r.Host + path } // RemoteIP strips the port from r.RemoteAddr for use as a rate-limit key. func RemoteIP(r *http.Request) string { host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return host }