negotiate.go (5557B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package util 19 20 import ( 21 "errors" 22 "fmt" 23 "strings" 24 25 "github.com/gin-gonic/gin" 26 ) 27 28 // ActivityPubAcceptHeaders represents the Accept headers mentioned here: 29 var ActivityPubAcceptHeaders = []MIME{ 30 AppActivityJSON, 31 AppActivityLDJSON, 32 } 33 34 // JSONAcceptHeaders is a slice of offers that just contains application/json types. 35 var JSONAcceptHeaders = []MIME{ 36 AppJSON, 37 } 38 39 // WebfingerJSONAcceptHeaders is a slice of offers that prefers the 40 // jrd+json content type, but will be chill and fall back to app/json. 41 // This is to be used specifically for webfinger responses. 42 // See https://www.rfc-editor.org/rfc/rfc7033#section-10.2 43 var WebfingerJSONAcceptHeaders = []MIME{ 44 AppJRDJSON, 45 AppJSON, 46 } 47 48 // JSONOrHTMLAcceptHeaders is a slice of offers that prefers AppJSON and will 49 // fall back to HTML if necessary. This is useful for error handling, since it can 50 // be used to serve a nice HTML page if the caller accepts that, or just JSON if not. 51 var JSONOrHTMLAcceptHeaders = []MIME{ 52 AppJSON, 53 TextHTML, 54 } 55 56 // HTMLAcceptHeaders is a slice of offers that just contains text/html types. 57 var HTMLAcceptHeaders = []MIME{ 58 TextHTML, 59 } 60 61 // HTMLOrActivityPubHeaders matches text/html first, then activitypub types. 62 // This is useful for user URLs that a user might go to in their browser. 63 // https://www.w3.org/TR/activitypub/#retrieving-objects 64 var HTMLOrActivityPubHeaders = []MIME{ 65 TextHTML, 66 AppActivityJSON, 67 AppActivityLDJSON, 68 } 69 70 var HostMetaHeaders = []MIME{ 71 AppXMLXRD, 72 AppXML, 73 } 74 75 // NegotiateAccept takes the *gin.Context from an incoming request, and a 76 // slice of Offers, and performs content negotiation for the given request 77 // with the given content-type offers. It will return a string representation 78 // of the first suitable content-type, or an error if something goes wrong or 79 // a suitable content-type cannot be matched. 80 // 81 // For example, if the request in the *gin.Context has Accept headers of value 82 // [application/json, text/html], and the provided offers are of value 83 // [application/json, application/xml], then the returned string will be 84 // 'application/json', which indicates the content-type that should be returned. 85 // 86 // If the length of offers is 0, then an error will be returned, so this function 87 // should only be called in places where format negotiation is actually needed. 88 // 89 // If there are no Accept headers in the request, then the first offer will be returned, 90 // under the assumption that it's better to serve *something* than error out completely. 91 // 92 // Callers can use the offer slices exported in this package as shortcuts for 93 // often-used Accept types. 94 // 95 // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation 96 func NegotiateAccept(c *gin.Context, offers ...MIME) (string, error) { 97 if len(offers) == 0 { 98 return "", errors.New("no format offered") 99 } 100 101 strings := []string{} 102 for _, o := range offers { 103 strings = append(strings, string(o)) 104 } 105 106 accepts := c.Request.Header.Values("Accept") 107 if len(accepts) == 0 { 108 // there's no accept header set, just return the first offer 109 return strings[0], nil 110 } 111 112 format := NegotiateFormat(c, strings...) 113 if format == "" { 114 return "", fmt.Errorf("no format can be offered for requested Accept header(s) %s; this endpoint offers %s", accepts, offers) 115 } 116 117 return format, nil 118 } 119 120 // This is the exact same thing as gin.Context.NegotiateFormat except it contains 121 // tsmethurst's fix to make it work properly with multiple accept headers. 122 // 123 // https://github.com/gin-gonic/gin/pull/3156 124 func NegotiateFormat(c *gin.Context, offered ...string) string { 125 if len(offered) == 0 { 126 panic("you must provide at least one offer") 127 } 128 129 if c.Accepted == nil { 130 for _, a := range c.Request.Header.Values("Accept") { 131 c.Accepted = append(c.Accepted, parseAccept(a)...) 132 } 133 } 134 if len(c.Accepted) == 0 { 135 return offered[0] 136 } 137 for _, accepted := range c.Accepted { 138 for _, offer := range offered { 139 // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers, 140 // therefore we can just iterate over the string without casting it into []rune 141 i := 0 142 for ; i < len(accepted); i++ { 143 if accepted[i] == '*' || offer[i] == '*' { 144 return offer 145 } 146 if accepted[i] != offer[i] { 147 break 148 } 149 } 150 if i == len(accepted) { 151 return offer 152 } 153 } 154 } 155 return "" 156 } 157 158 // https://github.com/gin-gonic/gin/blob/4787b8203b79012877ac98d7806422da3a678ba2/utils.go#L103 159 func parseAccept(acceptHeader string) []string { 160 parts := strings.Split(acceptHeader, ",") 161 out := make([]string, 0, len(parts)) 162 for _, part := range parts { 163 if i := strings.IndexByte(part, ';'); i > 0 { 164 part = part[:i] 165 } 166 if part = strings.TrimSpace(part); part != "" { 167 out = append(out, part) 168 } 169 } 170 return out 171 }