| 
									
										
										
										
											2023-03-12 16:00:57 +01:00
										 |  |  | // GoToSocial | 
					
						
							|  |  |  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | 
					
						
							|  |  |  | // SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // This program is free software: you can redistribute it and/or modify | 
					
						
							|  |  |  | // it under the terms of the GNU Affero General Public License as published by | 
					
						
							|  |  |  | // the Free Software Foundation, either version 3 of the License, or | 
					
						
							|  |  |  | // (at your option) any later version. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // This program is distributed in the hope that it will be useful, | 
					
						
							|  |  |  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | 
					
						
							|  |  |  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
					
						
							|  |  |  | // GNU Affero General Public License for more details. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // You should have received a copy of the GNU Affero General Public License | 
					
						
							|  |  |  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-02 13:10:50 +01:00
										 |  |  | package util | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							| 
									
										
										
										
											2023-05-16 15:08:45 +02:00
										 |  |  | 	"strings" | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/gin-gonic/gin" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // JSONAcceptHeaders is a slice of offers that just contains application/json types. | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | var JSONAcceptHeaders = []string{ | 
					
						
							| 
									
										
										
										
											2022-06-08 20:38:03 +02:00
										 |  |  | 	AppJSON, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-04 12:28:50 +02:00
										 |  |  | // WebfingerJSONAcceptHeaders is a slice of offers that prefers the | 
					
						
							|  |  |  | // jrd+json content type, but will be chill and fall back to app/json. | 
					
						
							|  |  |  | // This is to be used specifically for webfinger responses. | 
					
						
							|  |  |  | // See https://www.rfc-editor.org/rfc/rfc7033#section-10.2 | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | var WebfingerJSONAcceptHeaders = []string{ | 
					
						
							| 
									
										
										
										
											2023-05-04 12:28:50 +02:00
										 |  |  | 	AppJRDJSON, | 
					
						
							|  |  |  | 	AppJSON, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-12 08:16:41 +00:00
										 |  |  | // JSONOrHTMLAcceptHeaders is a slice of offers that prefers AppJSON and will | 
					
						
							|  |  |  | // fall back to HTML if necessary. This is useful for error handling, since it can | 
					
						
							| 
									
										
										
										
											2022-06-08 20:38:03 +02:00
										 |  |  | // be used to serve a nice HTML page if the caller accepts that, or just JSON if not. | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | var JSONOrHTMLAcceptHeaders = []string{ | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | 	AppJSON, | 
					
						
							| 
									
										
										
										
											2023-05-12 08:16:41 +00:00
										 |  |  | 	TextHTML, | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // HTMLAcceptHeaders is a slice of offers that just contains text/html types. | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | var HTMLAcceptHeaders = []string{ | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | 	TextHTML, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-06-08 20:38:03 +02:00
										 |  |  | // HTMLOrActivityPubHeaders matches text/html first, then activitypub types. | 
					
						
							| 
									
										
										
										
											2023-07-13 21:27:25 +02:00
										 |  |  | // This is useful for user URLs that a user might go to in their browser, | 
					
						
							|  |  |  | // but which should also be able to serve ActivityPub as a fallback. | 
					
						
							|  |  |  | // | 
					
						
							| 
									
										
										
										
											2022-06-08 20:38:03 +02:00
										 |  |  | // https://www.w3.org/TR/activitypub/#retrieving-objects | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | var HTMLOrActivityPubHeaders = []string{ | 
					
						
							| 
									
										
										
										
											2022-06-08 20:38:03 +02:00
										 |  |  | 	TextHTML, | 
					
						
							| 
									
										
										
										
											2023-07-13 21:27:25 +02:00
										 |  |  | 	AppActivityLDJSON, | 
					
						
							|  |  |  | 	AppActivityJSON, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // ActivityPubOrHTMLHeaders matches activitypub types first, then text/html. | 
					
						
							|  |  |  | // This is useful for URLs that should serve ActivityPub by default, but | 
					
						
							|  |  |  | // which a user might also go to in their browser sometimes. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // https://www.w3.org/TR/activitypub/#retrieving-objects | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | var ActivityPubOrHTMLHeaders = []string{ | 
					
						
							| 
									
										
										
										
											2023-07-13 21:27:25 +02:00
										 |  |  | 	AppActivityLDJSON, | 
					
						
							| 
									
										
										
										
											2022-06-08 20:38:03 +02:00
										 |  |  | 	AppActivityJSON, | 
					
						
							| 
									
										
										
										
											2023-07-13 21:27:25 +02:00
										 |  |  | 	TextHTML, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // ActivityPubHeaders matches only activitypub Accept headers. | 
					
						
							|  |  |  | // This is useful for URLs should only serve ActivityPub. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // https://www.w3.org/TR/activitypub/#retrieving-objects | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | var ActivityPubHeaders = []string{ | 
					
						
							| 
									
										
										
										
											2022-06-08 20:38:03 +02:00
										 |  |  | 	AppActivityLDJSON, | 
					
						
							| 
									
										
										
										
											2023-07-13 21:27:25 +02:00
										 |  |  | 	AppActivityJSON, | 
					
						
							| 
									
										
										
										
											2022-06-08 20:38:03 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | var HostMetaHeaders = []string{ | 
					
						
							| 
									
										
										
										
											2023-03-09 18:55:45 +01:00
										 |  |  | 	AppXMLXRD, | 
					
						
							|  |  |  | 	AppXML, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-31 16:03:34 +02:00
										 |  |  | // CSVHeaders just contains the text/csv | 
					
						
							|  |  |  | // MIME type, used for import/export. | 
					
						
							|  |  |  | var CSVHeaders = []string{ | 
					
						
							|  |  |  | 	TextCSV, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | // NegotiateAccept takes the *gin.Context from an incoming request, and a | 
					
						
							|  |  |  | // slice of Offers, and performs content negotiation for the given request | 
					
						
							|  |  |  | // with the given content-type offers. It will return a string representation | 
					
						
							|  |  |  | // of the first suitable content-type, or an error if something goes wrong or | 
					
						
							| 
									
										
										
										
											2021-12-20 11:08:54 +01:00
										 |  |  | // a suitable content-type cannot be matched. | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | // | 
					
						
							|  |  |  | // For example, if the request in the *gin.Context has Accept headers of value | 
					
						
							|  |  |  | // [application/json, text/html], and the provided offers are of value | 
					
						
							|  |  |  | // [application/json, application/xml], then the returned string will be | 
					
						
							|  |  |  | // 'application/json', which indicates the content-type that should be returned. | 
					
						
							|  |  |  | // | 
					
						
							| 
									
										
										
										
											2021-12-20 11:08:54 +01:00
										 |  |  | // If the length of offers is 0, then an error will be returned, so this function | 
					
						
							|  |  |  | // should only be called in places where format negotiation is actually needed. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // If there are no Accept headers in the request, then the first offer will be returned, | 
					
						
							|  |  |  | // under the assumption that it's better to serve *something* than error out completely. | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | // | 
					
						
							|  |  |  | // Callers can use the offer slices exported in this package as shortcuts for | 
					
						
							|  |  |  | // often-used Accept types. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation | 
					
						
							| 
									
										
										
										
											2023-11-27 14:00:57 +00:00
										 |  |  | func NegotiateAccept(c *gin.Context, offers ...string) (string, error) { | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | 	if len(offers) == 0 { | 
					
						
							|  |  |  | 		return "", errors.New("no format offered") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	strings := []string{} | 
					
						
							|  |  |  | 	for _, o := range offers { | 
					
						
							|  |  |  | 		strings = append(strings, string(o)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-20 11:08:54 +01:00
										 |  |  | 	accepts := c.Request.Header.Values("Accept") | 
					
						
							|  |  |  | 	if len(accepts) == 0 { | 
					
						
							|  |  |  | 		// there's no accept header set, just return the first offer | 
					
						
							|  |  |  | 		return strings[0], nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-16 15:08:45 +02:00
										 |  |  | 	format := NegotiateFormat(c, strings...) | 
					
						
							| 
									
										
										
										
											2021-12-11 17:50:00 +01:00
										 |  |  | 	if format == "" { | 
					
						
							|  |  |  | 		return "", fmt.Errorf("no format can be offered for requested Accept header(s) %s; this endpoint offers %s", accepts, offers) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return format, nil | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-05-16 15:08:45 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | // This is the exact same thing as gin.Context.NegotiateFormat except it contains | 
					
						
							|  |  |  | // tsmethurst's fix to make it work properly with multiple accept headers. | 
					
						
							|  |  |  | // | 
					
						
							|  |  |  | // https://github.com/gin-gonic/gin/pull/3156 | 
					
						
							|  |  |  | func NegotiateFormat(c *gin.Context, offered ...string) string { | 
					
						
							|  |  |  | 	if len(offered) == 0 { | 
					
						
							|  |  |  | 		panic("you must provide at least one offer") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if c.Accepted == nil { | 
					
						
							|  |  |  | 		for _, a := range c.Request.Header.Values("Accept") { | 
					
						
							|  |  |  | 			c.Accepted = append(c.Accepted, parseAccept(a)...) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if len(c.Accepted) == 0 { | 
					
						
							|  |  |  | 		return offered[0] | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	for _, accepted := range c.Accepted { | 
					
						
							|  |  |  | 		for _, offer := range offered { | 
					
						
							|  |  |  | 			// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers, | 
					
						
							|  |  |  | 			// therefore we can just iterate over the string without casting it into []rune | 
					
						
							|  |  |  | 			i := 0 | 
					
						
							|  |  |  | 			for ; i < len(accepted); i++ { | 
					
						
							|  |  |  | 				if accepted[i] == '*' || offer[i] == '*' { | 
					
						
							|  |  |  | 					return offer | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				if accepted[i] != offer[i] { | 
					
						
							|  |  |  | 					break | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if i == len(accepted) { | 
					
						
							|  |  |  | 				return offer | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return "" | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // https://github.com/gin-gonic/gin/blob/4787b8203b79012877ac98d7806422da3a678ba2/utils.go#L103 | 
					
						
							|  |  |  | func parseAccept(acceptHeader string) []string { | 
					
						
							|  |  |  | 	parts := strings.Split(acceptHeader, ",") | 
					
						
							|  |  |  | 	out := make([]string, 0, len(parts)) | 
					
						
							|  |  |  | 	for _, part := range parts { | 
					
						
							|  |  |  | 		if i := strings.IndexByte(part, ';'); i > 0 { | 
					
						
							|  |  |  | 			part = part[:i] | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if part = strings.TrimSpace(part); part != "" { | 
					
						
							|  |  |  | 			out = append(out, part) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return out | 
					
						
							|  |  |  | } |