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 "sort" 7 "strconv" 8 "strings" 9 ) 10 11 const ( 12 wcAny = "*" 13 matchAny = "*/*" 14 accept = "Accept" 15 acceptSplit = "," 16 partSplit = ";" 17 weightPrefix = "q=" 18 19 qValMultiplier = 1000 20 ) 21 22 type mimes []mime 23 24 func (m mimes) Len() int { 25 return len(m) 26 } 27 28 func (m mimes) Less(i, j int) bool { 29 return m[j].weight < m[i].weight 30 } 31 32 func (m mimes) Swap(i, j int) { 33 m[i], m[j] = m[j], m[i] 34 } 35 36 type mime struct { 37 mime Mime 38 weight uint16 39 } 40 41 // Mime represents a accepted Mime Type. 42 type Mime string 43 44 // Match checks to see whether a given Mime Type matches the value. 45 // 46 // The method allows for wildcards in the subtype sections. 47 func (m Mime) Match(n Mime) bool { 48 if strings.EqualFold(string(m), string(n)) || m == matchAny || n == matchAny { 49 return true 50 } 51 52 mParts := [2]string{wcAny, wcAny} 53 mPos := strings.IndexByte(string(m), '/') 54 55 if mPos < 0 { 56 mParts[0] = string(m) 57 } else { 58 mParts[0] = string(m[:mPos]) 59 mParts[1] = string(m[mPos+1:]) 60 } 61 62 nParts := [2]string{wcAny, wcAny} 63 nPos := strings.IndexByte(string(n), '/') 64 65 if nPos < 0 { 66 nParts[0] = string(n) 67 } else { 68 nParts[0] = string(n[:nPos]) 69 nParts[1] = string(n[nPos+1:]) 70 } 71 72 return strings.EqualFold(mParts[0], nParts[0]) && (strings.EqualFold(mParts[1], nParts[1]) || mParts[1] == wcAny || nParts[1] == wcAny) 73 } 74 75 // Handler provides an interface to handle a mime type. 76 // 77 // The mime string (e.g. text/html, application/json, text/plain) is passed to 78 // the handler, which is expected to return true if no more encodings are 79 // required and false otherwise. 80 // 81 // The empty string "" is used to signify when no preference is specified. 82 type Handler interface { 83 Handle(mime Mime) bool 84 } 85 86 // HandlerFunc wraps a func to make it satisfy the Handler interface. 87 type HandlerFunc func(Mime) bool 88 89 // Handle calls the underlying func. 90 func (h HandlerFunc) Handle(m Mime) bool { 91 return h(m) 92 } 93 94 // InvalidAccept writes the 406 header. 95 func InvalidAccept(w http.ResponseWriter) { 96 w.WriteHeader(http.StatusNotAcceptable) 97 } 98 99 // HandleAccept will process the Accept header and calls the given handler for 100 // each mime type until the handler returns true. 101 // 102 // This function returns true when the Handler returns true, false otherwise. 103 // 104 // When no Accept header is given the mime string will be the empty string. 105 func HandleAccept(r *http.Request, h Handler) bool { 106 accepts := parseAccepts(r.Header.Get(accept)) 107 108 if len(accepts) == 0 { 109 return h.Handle("") 110 } 111 112 sort.Stable(accepts) 113 114 for _, accept := range accepts { 115 if h.Handle(accept.mime) { 116 return true 117 } 118 } 119 120 return false 121 } 122 123 func parseAccepts(acceptHeader string) mimes { 124 acceptParts := strings.Split(acceptHeader, acceptSplit) 125 accepts := make(mimes, 0, len(acceptParts)) 126 127 Loop: 128 for _, accept := range acceptParts { 129 parts := strings.Split(strings.TrimSpace(accept), partSplit) 130 name := strings.ToLower(strings.TrimSpace(parts[0])) 131 132 if name == "" { 133 continue 134 } 135 136 qVal := float64(1) 137 138 var err error 139 140 for _, part := range parts[1:] { 141 if strings.HasPrefix(strings.TrimSpace(part), weightPrefix) { 142 if qVal, err = strconv.ParseFloat(part[len(weightPrefix):], 32); err != nil || qVal < 0 || qVal >= 2 { 143 continue Loop 144 } 145 146 break 147 } 148 } 149 150 accepts = append(accepts, mime{mime: Mime(name), weight: uint16(qVal * qValMultiplier)}) 151 } 152 153 return accepts 154 } 155