gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }