[mirror] Scalable static site server for Git forges (like GitHub Pages)
1package git_pages
2
3import (
4 "cmp"
5 "fmt"
6 "net"
7 "net/http"
8 "regexp"
9 "slices"
10 "strconv"
11 "strings"
12)
13
14var httpAcceptRegexp = regexp.MustCompile(`` +
15 // token optionally prefixed by whitespace
16 `^[ \t]*([a-zA-Z0-9$!#$%&'*+./^_\x60|~-]+)` +
17 // quality value prefixed by a semicolon optionally surrounded by whitespace
18 `(?:[ \t]*;[ \t]*q=(0(?:\.[0-9]{1,3})?|1(?:\.0{1,3})?))?` +
19 // optional whitespace followed by comma or end of line
20 `[ \t]*(?:,|$)`,
21)
22
23type httpAcceptOffer struct {
24 code string
25 qval float64
26}
27
28func parseGenericAcceptHeader(headerValue string) (result []httpAcceptOffer) {
29 for headerValue != "" {
30 matches := httpAcceptRegexp.FindStringSubmatch(headerValue)
31 if matches == nil {
32 return
33 }
34 offer := httpAcceptOffer{strings.ToLower(matches[1]), 1.0}
35 if matches[2] != "" {
36 offer.qval, _ = strconv.ParseFloat(matches[2], 64)
37 }
38 result = append(result, offer)
39 headerValue = headerValue[len(matches[0]):]
40 }
41 return
42}
43
44func preferredAcceptOffer(offers []httpAcceptOffer) string {
45 slices.SortStableFunc(offers, func(a, b httpAcceptOffer) int {
46 return -cmp.Compare(a.qval, b.qval)
47 })
48 for _, offer := range offers {
49 if offer.qval != 0 {
50 return offer.code
51 }
52 }
53 return ""
54}
55
56type HTTPContentTypes struct {
57 contentTypes []httpAcceptOffer
58}
59
60func ParseAcceptHeader(headerValue string) (result HTTPContentTypes) {
61 if headerValue == "" {
62 headerValue = "*/*"
63 }
64 result = HTTPContentTypes{parseGenericAcceptHeader(headerValue)}
65 return
66}
67
68func (e *HTTPContentTypes) Negotiate(offers ...string) string {
69 prefs := make(map[string]float64, len(offers))
70 for _, code := range offers {
71 prefs[code] = 0
72 }
73 for _, ctyp := range e.contentTypes {
74 if ctyp.code == "*/*" {
75 for code := range prefs {
76 prefs[code] = ctyp.qval
77 }
78 } else if _, ok := prefs[ctyp.code]; ok {
79 prefs[ctyp.code] = ctyp.qval
80 }
81 }
82 ctyps := make([]httpAcceptOffer, len(offers))
83 for idx, code := range offers {
84 ctyps[idx] = httpAcceptOffer{code, prefs[code]}
85 }
86 return preferredAcceptOffer(ctyps)
87}
88
89type HTTPEncodings struct {
90 encodings []httpAcceptOffer
91}
92
93func ParseAcceptEncodingHeader(headerValue string) (result HTTPEncodings) {
94 result = HTTPEncodings{parseGenericAcceptHeader(headerValue)}
95 if len(result.encodings) == 0 {
96 // RFC 9110 says (https://httpwg.org/specs/rfc9110.html#field.accept-encoding):
97 // "If no Accept-Encoding header field is in the request, any content
98 // coding is considered acceptable by the user agent."
99 // In practice, no client expects to receive a compressed response
100 // without having sent Accept-Encoding in the request.
101 }
102 return
103}
104
105// Negotiate returns the most preferred encoding that is acceptable by the
106// client, or an empty string if no encodings are acceptable.
107func (e *HTTPEncodings) Negotiate(offers ...string) string {
108 prefs := make(map[string]float64, len(offers))
109 for _, code := range offers {
110 prefs[code] = 0
111 }
112 implicitIdentity := true
113 for _, enc := range e.encodings {
114 if enc.code == "*" {
115 for code := range prefs {
116 prefs[code] = enc.qval
117 }
118 implicitIdentity = false
119 } else if _, ok := prefs[enc.code]; ok {
120 prefs[enc.code] = enc.qval
121 }
122 if enc.code == "*" || enc.code == "identity" {
123 implicitIdentity = false
124 }
125 }
126 if _, ok := prefs["identity"]; ok && implicitIdentity {
127 prefs["identity"] = -1 // sort last
128 }
129 encs := make([]httpAcceptOffer, len(offers))
130 for idx, code := range offers {
131 encs[idx] = httpAcceptOffer{code, prefs[code]}
132 }
133 return preferredAcceptOffer(encs)
134}
135
136func chainHTTPMiddleware(middleware ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
137 return func(handler http.Handler) http.Handler {
138 for idx := len(middleware) - 1; idx >= 0; idx-- {
139 handler = middleware[idx](handler)
140 }
141 return handler
142 }
143}
144
145func remoteAddrMiddleware(handler http.Handler) http.Handler {
146 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
147 var readXForwardedFor bool
148 switch config.Audit.IncludeIPs {
149 case "X-Forwarded-For":
150 readXForwardedFor = true
151 case "RemoteAddr", "":
152 readXForwardedFor = false
153 default:
154 panic(fmt.Errorf("config.Audit.IncludeIPs is set to an unknown value (%q)",
155 config.Audit.IncludeIPs))
156 }
157
158 usingOriginalRemoteAddr := true
159 if readXForwardedFor {
160 forwardedFor := strings.Split(r.Header.Get("X-Forwarded-For"), ",")
161 if len(forwardedFor) > 0 {
162 remoteAddr := strings.TrimSpace(forwardedFor[len(forwardedFor)-1])
163 if remoteAddr != "" {
164 r.RemoteAddr = remoteAddr
165 usingOriginalRemoteAddr = false
166 }
167 }
168 }
169 if usingOriginalRemoteAddr {
170 if ipAddress, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
171 r.RemoteAddr = ipAddress
172 }
173 }
174
175 handler.ServeHTTP(w, r)
176 })
177}