1 | package brotli
|
---|
2 |
|
---|
3 | import (
|
---|
4 | "compress/gzip"
|
---|
5 | "io"
|
---|
6 | "net/http"
|
---|
7 | "strings"
|
---|
8 | )
|
---|
9 |
|
---|
10 | // HTTPCompressor chooses a compression method (brotli, gzip, or none) based on
|
---|
11 | // the Accept-Encoding header, sets the Content-Encoding header, and returns a
|
---|
12 | // WriteCloser that implements that compression. The Close method must be called
|
---|
13 | // before the current HTTP handler returns.
|
---|
14 | //
|
---|
15 | // Due to https://github.com/golang/go/issues/31753, the response will not be
|
---|
16 | // compressed unless you set a Content-Type header before you call
|
---|
17 | // HTTPCompressor.
|
---|
18 | func HTTPCompressor(w http.ResponseWriter, r *http.Request) io.WriteCloser {
|
---|
19 | if w.Header().Get("Content-Type") == "" {
|
---|
20 | return nopCloser{w}
|
---|
21 | }
|
---|
22 |
|
---|
23 | if w.Header().Get("Vary") == "" {
|
---|
24 | w.Header().Set("Vary", "Accept-Encoding")
|
---|
25 | }
|
---|
26 |
|
---|
27 | encoding := negotiateContentEncoding(r, []string{"br", "gzip"})
|
---|
28 | switch encoding {
|
---|
29 | case "br":
|
---|
30 | w.Header().Set("Content-Encoding", "br")
|
---|
31 | return NewWriter(w)
|
---|
32 | case "gzip":
|
---|
33 | w.Header().Set("Content-Encoding", "gzip")
|
---|
34 | return gzip.NewWriter(w)
|
---|
35 | }
|
---|
36 | return nopCloser{w}
|
---|
37 | }
|
---|
38 |
|
---|
39 | // negotiateContentEncoding returns the best offered content encoding for the
|
---|
40 | // request's Accept-Encoding header. If two offers match with equal weight and
|
---|
41 | // then the offer earlier in the list is preferred. If no offers are
|
---|
42 | // acceptable, then "" is returned.
|
---|
43 | func negotiateContentEncoding(r *http.Request, offers []string) string {
|
---|
44 | bestOffer := "identity"
|
---|
45 | bestQ := -1.0
|
---|
46 | specs := parseAccept(r.Header, "Accept-Encoding")
|
---|
47 | for _, offer := range offers {
|
---|
48 | for _, spec := range specs {
|
---|
49 | if spec.Q > bestQ &&
|
---|
50 | (spec.Value == "*" || spec.Value == offer) {
|
---|
51 | bestQ = spec.Q
|
---|
52 | bestOffer = offer
|
---|
53 | }
|
---|
54 | }
|
---|
55 | }
|
---|
56 | if bestQ == 0 {
|
---|
57 | bestOffer = ""
|
---|
58 | }
|
---|
59 | return bestOffer
|
---|
60 | }
|
---|
61 |
|
---|
62 | // acceptSpec describes an Accept* header.
|
---|
63 | type acceptSpec struct {
|
---|
64 | Value string
|
---|
65 | Q float64
|
---|
66 | }
|
---|
67 |
|
---|
68 | // parseAccept parses Accept* headers.
|
---|
69 | func parseAccept(header http.Header, key string) (specs []acceptSpec) {
|
---|
70 | loop:
|
---|
71 | for _, s := range header[key] {
|
---|
72 | for {
|
---|
73 | var spec acceptSpec
|
---|
74 | spec.Value, s = expectTokenSlash(s)
|
---|
75 | if spec.Value == "" {
|
---|
76 | continue loop
|
---|
77 | }
|
---|
78 | spec.Q = 1.0
|
---|
79 | s = skipSpace(s)
|
---|
80 | if strings.HasPrefix(s, ";") {
|
---|
81 | s = skipSpace(s[1:])
|
---|
82 | if !strings.HasPrefix(s, "q=") {
|
---|
83 | continue loop
|
---|
84 | }
|
---|
85 | spec.Q, s = expectQuality(s[2:])
|
---|
86 | if spec.Q < 0.0 {
|
---|
87 | continue loop
|
---|
88 | }
|
---|
89 | }
|
---|
90 | specs = append(specs, spec)
|
---|
91 | s = skipSpace(s)
|
---|
92 | if !strings.HasPrefix(s, ",") {
|
---|
93 | continue loop
|
---|
94 | }
|
---|
95 | s = skipSpace(s[1:])
|
---|
96 | }
|
---|
97 | }
|
---|
98 | return
|
---|
99 | }
|
---|
100 |
|
---|
101 | func skipSpace(s string) (rest string) {
|
---|
102 | i := 0
|
---|
103 | for ; i < len(s); i++ {
|
---|
104 | if octetTypes[s[i]]&isSpace == 0 {
|
---|
105 | break
|
---|
106 | }
|
---|
107 | }
|
---|
108 | return s[i:]
|
---|
109 | }
|
---|
110 |
|
---|
111 | func expectTokenSlash(s string) (token, rest string) {
|
---|
112 | i := 0
|
---|
113 | for ; i < len(s); i++ {
|
---|
114 | b := s[i]
|
---|
115 | if (octetTypes[b]&isToken == 0) && b != '/' {
|
---|
116 | break
|
---|
117 | }
|
---|
118 | }
|
---|
119 | return s[:i], s[i:]
|
---|
120 | }
|
---|
121 |
|
---|
122 | func expectQuality(s string) (q float64, rest string) {
|
---|
123 | switch {
|
---|
124 | case len(s) == 0:
|
---|
125 | return -1, ""
|
---|
126 | case s[0] == '0':
|
---|
127 | q = 0
|
---|
128 | case s[0] == '1':
|
---|
129 | q = 1
|
---|
130 | default:
|
---|
131 | return -1, ""
|
---|
132 | }
|
---|
133 | s = s[1:]
|
---|
134 | if !strings.HasPrefix(s, ".") {
|
---|
135 | return q, s
|
---|
136 | }
|
---|
137 | s = s[1:]
|
---|
138 | i := 0
|
---|
139 | n := 0
|
---|
140 | d := 1
|
---|
141 | for ; i < len(s); i++ {
|
---|
142 | b := s[i]
|
---|
143 | if b < '0' || b > '9' {
|
---|
144 | break
|
---|
145 | }
|
---|
146 | n = n*10 + int(b) - '0'
|
---|
147 | d *= 10
|
---|
148 | }
|
---|
149 | return q + float64(n)/float64(d), s[i:]
|
---|
150 | }
|
---|
151 |
|
---|
152 | // Octet types from RFC 2616.
|
---|
153 | var octetTypes [256]octetType
|
---|
154 |
|
---|
155 | type octetType byte
|
---|
156 |
|
---|
157 | const (
|
---|
158 | isToken octetType = 1 << iota
|
---|
159 | isSpace
|
---|
160 | )
|
---|
161 |
|
---|
162 | func init() {
|
---|
163 | // OCTET = <any 8-bit sequence of data>
|
---|
164 | // CHAR = <any US-ASCII character (octets 0 - 127)>
|
---|
165 | // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
---|
166 | // CR = <US-ASCII CR, carriage return (13)>
|
---|
167 | // LF = <US-ASCII LF, linefeed (10)>
|
---|
168 | // SP = <US-ASCII SP, space (32)>
|
---|
169 | // HT = <US-ASCII HT, horizontal-tab (9)>
|
---|
170 | // <"> = <US-ASCII double-quote mark (34)>
|
---|
171 | // CRLF = CR LF
|
---|
172 | // LWS = [CRLF] 1*( SP | HT )
|
---|
173 | // TEXT = <any OCTET except CTLs, but including LWS>
|
---|
174 | // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
---|
175 | // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
---|
176 | // token = 1*<any CHAR except CTLs or separators>
|
---|
177 | // qdtext = <any TEXT except <">>
|
---|
178 |
|
---|
179 | for c := 0; c < 256; c++ {
|
---|
180 | var t octetType
|
---|
181 | isCtl := c <= 31 || c == 127
|
---|
182 | isChar := 0 <= c && c <= 127
|
---|
183 | isSeparator := strings.ContainsRune(" \t\"(),/:;<=>?@[]\\{}", rune(c))
|
---|
184 | if strings.ContainsRune(" \t\r\n", rune(c)) {
|
---|
185 | t |= isSpace
|
---|
186 | }
|
---|
187 | if isChar && !isCtl && !isSeparator {
|
---|
188 | t |= isToken
|
---|
189 | }
|
---|
190 | octetTypes[c] = t
|
---|
191 | }
|
---|
192 | }
|
---|