1 // Package httpaccept provides a function to deal with the Accept header. 2 package httpaccept // import "vimagination.zapto.org/httpaccept" 3 4 import ( 5 "net/http" 6 "slices" 7 "sort" 8 "strings" 9 10 "vimagination.zapto.org/parser" 11 ) 12 13 const ( 14 wcAny = "*" 15 matchAny = "*/*" 16 accept = "Accept" 17 ) 18 19 type mimes []mime 20 21 func (m mimes) Len() int { 22 return len(m) 23 } 24 25 func (m mimes) Less(i, j int) bool { 26 return m[j].weight < m[i].weight 27 } 28 29 func (m mimes) Swap(i, j int) { 30 m[i], m[j] = m[j], m[i] 31 } 32 33 type mime struct { 34 mime Mime 35 weight int16 36 } 37 38 // Mime represents a accepted Mime Type. 39 type Mime string 40 41 // Match checks to see whether a given Mime Type matches the value. 42 // 43 // The method allows for wildcards in the subtype sections. 44 func (m Mime) Match(n Mime) bool { 45 mMime, mExcl, _ := strings.Cut(string(m), ";") 46 nMime, nExcl, _ := strings.Cut(string(n), ";") 47 mPrefix, mSuffix, _ := strings.Cut(mMime, "/") 48 nPrefix, nSuffix, _ := strings.Cut(nMime, "/") 49 50 if mPrefix != wcAny && nPrefix != wcAny && !strings.EqualFold(mPrefix, nPrefix) { 51 return false 52 } 53 54 if mSuffix != wcAny && nSuffix != wcAny && !strings.EqualFold(mSuffix, nSuffix) { 55 return false 56 } 57 58 if mExcl != "" { 59 for mEx := range strings.SplitSeq(mExcl, ";") { 60 if Mime(nMime).Match(Mime(mEx)) { 61 return false 62 } 63 } 64 } 65 66 if nExcl != "" { 67 for nEx := range strings.SplitSeq(nExcl, ";") { 68 if Mime(mMime).Match(Mime(nEx)) { 69 return false 70 } 71 } 72 } 73 74 return true 75 } 76 77 // Handler provides an interface to handle a mime type. 78 // 79 // The mime string (e.g. text/html, application/json, text/plain) is passed to 80 // the handler, which is expected to return true if no more encodings are 81 // required and false otherwise. 82 // 83 // The empty string "" is used to signify when no preference is specified. 84 type Handler interface { 85 Handle(mime Mime) bool 86 } 87 88 // HandlerFunc wraps a func to make it satisfy the Handler interface. 89 type HandlerFunc func(Mime) bool 90 91 // Handle calls the underlying func. 92 func (h HandlerFunc) Handle(m Mime) bool { 93 return h(m) 94 } 95 96 // InvalidAccept writes the 406 header. 97 func InvalidAccept(w http.ResponseWriter) { 98 w.WriteHeader(http.StatusNotAcceptable) 99 } 100 101 // HandleAccept will process the Accept header and calls the given handler for 102 // each mime type until the handler returns true. 103 // 104 // This function returns true when the Handler returns true, false otherwise. 105 // 106 // Wildcard matches will be followed by a semi-colon delimited string of the 107 // exclusions. 108 // 109 // When no Accept header is given the mime string will be the empty string. 110 func HandleAccept(r *http.Request, h Handler) bool { 111 accepts := parseAccepts(r.Header.Get(accept)) 112 113 if len(accepts) == 0 { 114 return h.Handle("") 115 } 116 117 sort.Stable(accepts) 118 119 for _, accept := range accepts { 120 if accept.weight > 0 && h.Handle(accept.mime) { 121 return true 122 } 123 } 124 125 return false 126 } 127 128 func parseAccepts(acceptHeader string) mimes { 129 accepts := make(mimes, 0, strings.Count(acceptHeader, delim)+1) 130 131 p := parseAccept(acceptHeader) 132 133 for { 134 coding := p.Next() 135 if coding.Type == parser.TokenDone { 136 break 137 } 138 139 name := coding.Data 140 141 if p.Accept(tokenInvalidWeight) { 142 continue 143 } 144 145 weight := int16(1000) 146 147 if p.Peek().Type == tokenWeight { 148 weight = parseQ(p.Next().Data) 149 } 150 151 if slices.ContainsFunc(accepts, func(e mime) bool { return e.mime == Mime(name) }) { 152 continue 153 } 154 155 accepts = append(accepts, mime{mime: Mime(name), weight: weight}) 156 } 157 158 return processAnys(accepts) 159 } 160 161 func processAnys(accepts mimes) mimes { 162 for n := range accepts { 163 if accepts[n].weight == 0 { 164 continue 165 } 166 167 if prefix, suffix, _ := strings.Cut(string(accepts[n].mime), "/"); prefix != wcAny && suffix != wcAny { 168 continue 169 } 170 171 var nots strings.Builder 172 173 nots.WriteString(string(accepts[n].mime)) 174 175 for _, m := range accepts { 176 if m.weight > 0 { 177 continue 178 } 179 180 if accepts[n].mime.Match(m.mime) { 181 nots.WriteByte(';') 182 nots.WriteString(string(m.mime)) 183 } 184 } 185 186 if nots.Len() > len(accepts[n].mime) { 187 accepts[n].mime = Mime(nots.String()) 188 } 189 } 190 191 return accepts 192 } 193 194 var multiplies = [...]int16{100, 10, 1} 195 196 func parseQ(q string) int16 { 197 if q[0] == '1' { 198 return 1000 199 } 200 201 if len(q) < 2 { 202 return 0 203 } 204 205 var qv int16 206 207 for n, v := range q[2:] { 208 qv += int16(v-'0') * multiplies[n] 209 } 210 211 return qv 212 } 213