Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
internal/httpx/httpx.go
79436f09f5539aac9b6f836b5b9049bbbece48aa
· 2.6 KB · 85 lines
raw
| 1 | // Package httpx holds small HTTP helpers shared by gsg's non-page |
| 2 | // surfaces. The admin and share handlers must make the same origin, |
| 3 | // scheme, header and remote-address decisions; keeping them here means a |
| 4 | // security fix lands in one place instead of three. |
| 5 | package httpx |
| 6 | |
| 7 | import ( |
| 8 | "net" |
| 9 | "net/http" |
| 10 | "net/url" |
| 11 | "strings" |
| 12 | ) |
| 13 | |
| 14 | // SecureOptions controls the admin/share response policy. These pages are |
| 15 | // deliberately not the themed site: no inline script or style, so the CSP |
| 16 | // can stay tight. |
| 17 | type SecureOptions struct { |
| 18 | // ScriptSrc is the CSP script-src value. Empty means "'none'"; the |
| 19 | // notices admin page needs "'self'" for admin.js, while share pages |
| 20 | // ship no JavaScript at all. |
| 21 | ScriptSrc string |
| 22 | // NoStore marks a response whose body must never be stored, even by |
| 23 | // the requesting browser -- a minted share URL is a bearer capability. |
| 24 | NoStore bool |
| 25 | } |
| 26 | |
| 27 | // Secure sets the response headers every admin/share page must carry. |
| 28 | func Secure(w http.ResponseWriter, opts SecureOptions) { |
| 29 | scriptSrc := opts.ScriptSrc |
| 30 | if scriptSrc == "" { |
| 31 | scriptSrc = "'none'" |
| 32 | } |
| 33 | hdr := w.Header() |
| 34 | hdr.Set("X-Content-Type-Options", "nosniff") |
| 35 | hdr.Set("X-Frame-Options", "DENY") |
| 36 | hdr.Set("Content-Security-Policy", |
| 37 | "default-src 'none'; style-src 'self'; script-src "+scriptSrc+ |
| 38 | "; img-src 'self'; form-action 'self'; base-uri 'none'") |
| 39 | if opts.NoStore { |
| 40 | hdr.Set("Cache-Control", "no-store") |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // SameOrigin blocks a cross-site POST even if a session cookie would |
| 45 | // otherwise be sent. A valid cookie proves the browser is logged in; it |
| 46 | // does not prove the request came from our own page. |
| 47 | func SameOrigin(r *http.Request) bool { |
| 48 | switch r.Header.Get("Sec-Fetch-Site") { |
| 49 | case "", "same-origin", "none": |
| 50 | default: |
| 51 | return false |
| 52 | } |
| 53 | if o := r.Header.Get("Origin"); o != "" { |
| 54 | u, err := url.Parse(o) |
| 55 | if err != nil || !strings.EqualFold(u.Host, r.Host) { |
| 56 | return false |
| 57 | } |
| 58 | } |
| 59 | return true |
| 60 | } |
| 61 | |
| 62 | // IsHTTPS reports whether the browser's original request was HTTPS, |
| 63 | // accounting for the reverse proxy's X-Forwarded-Proto. |
| 64 | func IsHTTPS(r *http.Request) bool { |
| 65 | return r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") |
| 66 | } |
| 67 | |
| 68 | // AbsoluteURL builds an absolute URL for a path on this host, using the |
| 69 | // same proxy-aware scheme as IsHTTPS. |
| 70 | func AbsoluteURL(r *http.Request, path string) string { |
| 71 | scheme := "http" |
| 72 | if IsHTTPS(r) { |
| 73 | scheme = "https" |
| 74 | } |
| 75 | return scheme + "://" + r.Host + path |
| 76 | } |
| 77 | |
| 78 | // RemoteIP strips the port from r.RemoteAddr for use as a rate-limit key. |
| 79 | func RemoteIP(r *http.Request) string { |
| 80 | host, _, err := net.SplitHostPort(r.RemoteAddr) |
| 81 | if err != nil { |
| 82 | return r.RemoteAddr |
| 83 | } |
| 84 | return host |
| 85 | } |