1 // Package httpencoding provides a function to deal with the Accept-Encoding 2 // header. 3 package httpencoding // import "vimagination.zapto.org/httpencoding" 4 5 import ( 6 "net/http" 7 "slices" 8 "sort" 9 "strings" 10 11 "vimagination.zapto.org/parser" 12 ) 13 14 const ( 15 acceptEncoding = "Accept-Encoding" 16 anyEncoding = "*" 17 identityEncoding = "identity" 18 acceptSplit = "," 19 partSplit = ';' 20 weightPrefix = "q=" 21 ) 22 23 type encodings []encoding 24 25 func (e encodings) Len() int { 26 return len(e) 27 } 28 29 func (e encodings) Less(i, j int) bool { 30 return e[j].weight < e[i].weight 31 } 32 33 func (e encodings) Swap(i, j int) { 34 e[i], e[j] = e[j], e[i] 35 } 36 37 type encoding struct { 38 encoding Encoding 39 weight int16 40 } 41 42 // Encoding represents an encoding string as used by the client. Examples are 43 // gzip, br and deflate. 44 type Encoding string 45 46 // Handler provides an interface to handle an encoding. 47 // 48 // The encoding string (e.g. gzip, br, deflate) is passed to the handler, which 49 // is expected to return true if no more encodings are required and false 50 // otherwise. 51 // 52 // The empty string "" is used to signify the identity encoding, or plain text. 53 type Handler interface { 54 Handle(encoding Encoding) bool 55 } 56 57 // HandlerFunc wraps a func to make it satisfy the Handler interface. 58 type HandlerFunc func(Encoding) bool 59 60 // Handle calls the underlying func. 61 func (h HandlerFunc) Handle(e Encoding) bool { 62 return h(e) 63 } 64 65 // InvalidEncoding writes the 406 header. 66 func InvalidEncoding(w http.ResponseWriter) { 67 w.WriteHeader(http.StatusNotAcceptable) 68 } 69 70 // HandleEncoding will process the Accept-Encoding header and calls the given 71 // handler for each encoding until the handler returns true. 72 // 73 // This function returns true when the Handler returns true, false otherwise. 74 // 75 // For the identity (plain text) encoding the encoding string will be the 76 // empty string. 77 // 78 // The wildcard encoding (*) will, after the '*', contain a semi-colon separated 79 // list of all disallowed encodings (q=0). 80 func HandleEncoding(r *http.Request, h Handler) bool { 81 acceptHeader := strings.TrimSpace(r.Header.Get(acceptEncoding)) 82 83 if len(acceptHeader) == 0 { 84 acceptHeader = anyEncoding 85 } 86 87 for _, accept := range parseAccepts(acceptHeader) { 88 if accept.weight != 0 && h.Handle(accept.encoding) { 89 return true 90 } 91 } 92 93 return false 94 } 95 96 func parseAccepts(acceptHeader string) []encoding { 97 accepts := make(encodings, 0, strings.Count(acceptHeader, acceptSplit)+2) 98 hasIdentity := false 99 hasNoAny := false 100 anyPos := -1 101 102 var nots strings.Builder 103 104 nots.WriteString("*") 105 106 p := parseAccept(acceptHeader) 107 108 for { 109 coding := p.Next() 110 if coding.Type == parser.TokenDone { 111 break 112 } 113 114 name := coding.Data 115 116 if p.Accept(tokenInvalidWeight) { 117 continue 118 } 119 120 weight := int16(1000) 121 122 if p.Peek().Type == tokenWeight { 123 weight = parseQ(p.Next().Data) 124 } 125 126 if name == identityEncoding { 127 hasIdentity = true 128 name = "" 129 } else if name == anyEncoding && weight == 0 { 130 hasNoAny = true 131 } 132 133 if slices.ContainsFunc(accepts, func(e encoding) bool { return e.encoding == Encoding(name) }) { 134 continue 135 } 136 137 if weight == 0 { 138 nots.WriteByte(';') 139 nots.WriteString(name) 140 } 141 142 if name == anyEncoding { 143 if anyPos != -1 { 144 continue 145 } 146 147 anyPos = len(accepts) 148 } 149 150 accepts = append(accepts, encoding{ 151 encoding: Encoding(name), 152 weight: weight, 153 }) 154 } 155 156 if anyPos != -1 { 157 accepts[anyPos].encoding = Encoding(nots.String()) 158 } 159 160 sort.Stable(accepts) 161 162 if !hasIdentity && !hasNoAny { 163 accepts = append(accepts, encoding{ 164 encoding: "", 165 weight: 1, 166 }) 167 } 168 169 return accepts 170 } 171 172 var multiplies = [...]int16{100, 10, 1} 173 174 func parseQ(q string) int16 { 175 if q[0] == '1' { 176 return 1000 177 } 178 179 if len(q) < 2 { 180 return 0 181 } 182 183 var qv int16 184 185 for n, v := range q[2:] { 186 qv += int16(v-'0') * multiplies[n] 187 } 188 189 return qv 190 } 191 192 // ClearEncoding removes the Accept-Encoding header so that any further 193 // attempts to establish an encoding will simply used the default, plain text, 194 // encoding. 195 // 196 // Useful when you don't want a handler down the chain to also handle encoding. 197 func ClearEncoding(r *http.Request) { 198 r.Header.Del(acceptEncoding) 199 } 200 201 // IsDisallowedInWildcard will return true if the given encoding is disallowed 202 // in the given accept string. 203 func IsDisallowedInWildcard(accept, encoding Encoding) bool { 204 if !strings.HasPrefix(string(accept), "*;") { 205 return false 206 } 207 208 for enc := range strings.SplitSeq(string(accept[2:]), ";") { 209 if enc == string(encoding) { 210 return true 211 } 212 } 213 214 return false 215 } 216 217 // IsWildcard returns true when the given accept string is a wildcard match. 218 func IsWildcard(accept Encoding) bool { 219 return accept == "*" || strings.HasPrefix(string(accept), "*;") 220 } 221