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 }