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