diff options
Diffstat (limited to 'internal/api/util')
-rw-r--r-- | internal/api/util/errorhandling.go | 12 | ||||
-rw-r--r-- | internal/api/util/mime.go | 28 | ||||
-rw-r--r-- | internal/api/util/negotiate.go | 18 | ||||
-rw-r--r-- | internal/api/util/negotiate_test.go | 2 | ||||
-rw-r--r-- | internal/api/util/response.go | 282 |
5 files changed, 313 insertions, 29 deletions
diff --git a/internal/api/util/errorhandling.go b/internal/api/util/errorhandling.go index 4fa544ffd..8bb251040 100644 --- a/internal/api/util/errorhandling.go +++ b/internal/api/util/errorhandling.go @@ -55,7 +55,9 @@ func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context) (*api "requestID": gtscontext.RequestID(ctx), }) default: - c.JSON(http.StatusNotFound, gin.H{"error": errWithCode.Safe()}) + JSON(c, http.StatusNotFound, map[string]string{ + "error": errWithCode.Safe(), + }) } } @@ -78,7 +80,9 @@ func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context) ( "requestID": gtscontext.RequestID(ctx), }) default: - c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + JSON(c, errWithCode.Code(), map[string]string{ + "error": errWithCode.Safe(), + }) } } @@ -102,7 +106,7 @@ func ErrorHandler( c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode), - offers ...MIME, + offers ...string, ) { if ctxErr := c.Request.Context().Err(); ctxErr != nil { // Context error means either client has left already, @@ -175,7 +179,7 @@ func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) { l.Debug("handling OAuth error") } - c.JSON(statusCode, gin.H{ + JSON(c, statusCode, map[string]string{ "error": errWithCode.Error(), "error_description": errWithCode.Safe(), }) diff --git a/internal/api/util/mime.go b/internal/api/util/mime.go index edd0dcecf..ad1b405cd 100644 --- a/internal/api/util/mime.go +++ b/internal/api/util/mime.go @@ -17,20 +17,18 @@ package util -// MIME represents a mime-type. -type MIME string - const ( - AppJSON MIME = `application/json` - AppXML MIME = `application/xml` - AppXMLXRD MIME = `application/xrd+xml` - AppRSSXML MIME = `application/rss+xml` - AppActivityJSON MIME = `application/activity+json` - AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` - AppJRDJSON MIME = `application/jrd+json` // https://www.rfc-editor.org/rfc/rfc7033#section-10.2 - AppForm MIME = `application/x-www-form-urlencoded` - MultipartForm MIME = `multipart/form-data` - TextXML MIME = `text/xml` - TextHTML MIME = `text/html` - TextCSS MIME = `text/css` + // Possible GoToSocial mimetypes. + AppJSON = `application/json` + AppXML = `application/xml` + AppXMLXRD = `application/xrd+xml` + AppRSSXML = `application/rss+xml` + AppActivityJSON = `application/activity+json` + AppActivityLDJSON = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` + AppJRDJSON = `application/jrd+json` // https://www.rfc-editor.org/rfc/rfc7033#section-10.2 + AppForm = `application/x-www-form-urlencoded` + MultipartForm = `multipart/form-data` + TextXML = `text/xml` + TextHTML = `text/html` + TextCSS = `text/css` ) diff --git a/internal/api/util/negotiate.go b/internal/api/util/negotiate.go index f4268511b..5b4f54bc6 100644 --- a/internal/api/util/negotiate.go +++ b/internal/api/util/negotiate.go @@ -26,7 +26,7 @@ import ( ) // JSONAcceptHeaders is a slice of offers that just contains application/json types. -var JSONAcceptHeaders = []MIME{ +var JSONAcceptHeaders = []string{ AppJSON, } @@ -34,7 +34,7 @@ var JSONAcceptHeaders = []MIME{ // 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 -var WebfingerJSONAcceptHeaders = []MIME{ +var WebfingerJSONAcceptHeaders = []string{ AppJRDJSON, AppJSON, } @@ -42,13 +42,13 @@ var WebfingerJSONAcceptHeaders = []MIME{ // 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 // be used to serve a nice HTML page if the caller accepts that, or just JSON if not. -var JSONOrHTMLAcceptHeaders = []MIME{ +var JSONOrHTMLAcceptHeaders = []string{ AppJSON, TextHTML, } // HTMLAcceptHeaders is a slice of offers that just contains text/html types. -var HTMLAcceptHeaders = []MIME{ +var HTMLAcceptHeaders = []string{ TextHTML, } @@ -57,7 +57,7 @@ var HTMLAcceptHeaders = []MIME{ // but which should also be able to serve ActivityPub as a fallback. // // https://www.w3.org/TR/activitypub/#retrieving-objects -var HTMLOrActivityPubHeaders = []MIME{ +var HTMLOrActivityPubHeaders = []string{ TextHTML, AppActivityLDJSON, AppActivityJSON, @@ -68,7 +68,7 @@ var HTMLOrActivityPubHeaders = []MIME{ // which a user might also go to in their browser sometimes. // // https://www.w3.org/TR/activitypub/#retrieving-objects -var ActivityPubOrHTMLHeaders = []MIME{ +var ActivityPubOrHTMLHeaders = []string{ AppActivityLDJSON, AppActivityJSON, TextHTML, @@ -78,12 +78,12 @@ var ActivityPubOrHTMLHeaders = []MIME{ // This is useful for URLs should only serve ActivityPub. // // https://www.w3.org/TR/activitypub/#retrieving-objects -var ActivityPubHeaders = []MIME{ +var ActivityPubHeaders = []string{ AppActivityLDJSON, AppActivityJSON, } -var HostMetaHeaders = []MIME{ +var HostMetaHeaders = []string{ AppXMLXRD, AppXML, } @@ -109,7 +109,7 @@ var HostMetaHeaders = []MIME{ // often-used Accept types. // // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation -func NegotiateAccept(c *gin.Context, offers ...MIME) (string, error) { +func NegotiateAccept(c *gin.Context, offers ...string) (string, error) { if len(offers) == 0 { return "", errors.New("no format offered") } diff --git a/internal/api/util/negotiate_test.go b/internal/api/util/negotiate_test.go index a8b28b55f..d1b08695f 100644 --- a/internal/api/util/negotiate_test.go +++ b/internal/api/util/negotiate_test.go @@ -9,7 +9,7 @@ import ( "github.com/gin-gonic/gin" ) -type testMIMES []MIME +type testMIMES []string func (tm testMIMES) String(t *testing.T) string { t.Helper() diff --git a/internal/api/util/response.go b/internal/api/util/response.go new file mode 100644 index 000000000..e22bac545 --- /dev/null +++ b/internal/api/util/response.go @@ -0,0 +1,282 @@ +// 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/>. + +package util + +import ( + "encoding/json" + "encoding/xml" + "io" + "net/http" + "strconv" + "sync" + + "codeberg.org/gruf/go-byteutil" + "codeberg.org/gruf/go-fastcopy" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/log" +) + +var ( + // Pre-preared response body data. + StatusOKJSON = mustJSON(map[string]string{ + "status": http.StatusText(http.StatusOK), + }) + StatusAcceptedJSON = mustJSON(map[string]string{ + "status": http.StatusText(http.StatusAccepted), + }) + StatusInternalServerErrorJSON = mustJSON(map[string]string{ + "status": http.StatusText(http.StatusInternalServerError), + }) + EmptyJSONObject = mustJSON("{}") + EmptyJSONArray = mustJSON("[]") + + // write buffer pool. + bufPool sync.Pool +) + +// JSON calls EncodeJSONResponse() using gin.Context{}, with content-type = AppJSON, +// This function handles the case of JSON unmarshal errors and pools read buffers. +func JSON(c *gin.Context, code int, data any) { + EncodeJSONResponse(c.Writer, c.Request, code, AppJSON, data) +} + +// JSON calls EncodeJSONResponse() using gin.Context{}, with given content-type. +// This function handles the case of JSON unmarshal errors and pools read buffers. +func JSONType(c *gin.Context, code int, contentType string, data any) { + EncodeJSONResponse(c.Writer, c.Request, code, contentType, data) +} + +// Data calls WriteResponseBytes() using gin.Context{}, with given content-type. +func Data(c *gin.Context, code int, contentType string, data []byte) { + WriteResponseBytes(c.Writer, c.Request, code, contentType, data) +} + +// WriteResponse buffered streams 'data' as HTTP response +// to ResponseWriter with given status code content-type. +func WriteResponse( + rw http.ResponseWriter, + r *http.Request, + statusCode int, + contentType string, + data io.Reader, + length int64, +) { + if length < 0 { + // The worst-case scenario, length is not known so we need to + // read the entire thing into memory to know length & respond. + writeResponseUnknownLength(rw, r, statusCode, contentType, data) + return + } + + // The best-case scenario, stream content of known length. + rw.Header().Set("Content-Type", contentType) + rw.Header().Set("Content-Length", strconv.FormatInt(length, 10)) + rw.WriteHeader(statusCode) + if _, err := fastcopy.Copy(rw, data); err != nil { + log.Errorf(r.Context(), "error streaming: %v", err) + } +} + +// WriteResponseBytes is functionally similar to +// WriteResponse except that it takes prepared bytes. +func WriteResponseBytes( + rw http.ResponseWriter, + r *http.Request, + statusCode int, + contentType string, + data []byte, +) { + rw.Header().Set("Content-Type", contentType) + rw.Header().Set("Content-Length", strconv.Itoa(len(data))) + rw.WriteHeader(statusCode) + if _, err := rw.Write(data); err != nil && err != io.EOF { + log.Errorf(r.Context(), "error writing: %v", err) + } +} + +// EncodeJSONResponse encodes 'data' as JSON HTTP response +// to ResponseWriter with given status code, content-type. +func EncodeJSONResponse( + rw http.ResponseWriter, + r *http.Request, + statusCode int, + contentType string, + data any, +) { + // Acquire buffer. + buf := getBuf() + + // Wrap buffer in JSON encoder. + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + + // Encode JSON data into byte buffer. + if err := enc.Encode(data); err == nil { + + // Drop new-line added by encoder. + if buf.B[len(buf.B)-1] == '\n' { + buf.B = buf.B[:len(buf.B)-1] + } + + // Respond with the now-known + // size byte slice within buf. + WriteResponseBytes(rw, r, + statusCode, + contentType, + buf.B, + ) + } else { + // This will always be a JSON error, we + // can't really add any more useful context. + log.Error(r.Context(), err) + + // Any error returned here is unrecoverable, + // set Internal Server Error JSON response. + WriteResponseBytes(rw, r, + http.StatusInternalServerError, + AppJSON, + StatusInternalServerErrorJSON, + ) + } + + // Release. + putBuf(buf) +} + +// EncodeJSONResponse encodes 'data' as XML HTTP response +// to ResponseWriter with given status code, content-type. +func EncodeXMLResponse( + rw http.ResponseWriter, + r *http.Request, + statusCode int, + contentType string, + data any, +) { + // Acquire buffer. + buf := getBuf() + + // Write XML header string to buf. + buf.B = append(buf.B, xml.Header...) + + // Wrap buffer in XML encoder. + enc := xml.NewEncoder(buf) + + // Encode JSON data into byte buffer. + if err := enc.Encode(data); err == nil { + + // Respond with the now-known + // size byte slice within buf. + WriteResponseBytes(rw, r, + statusCode, + contentType, + buf.B, + ) + } else { + // This will always be an XML error, we + // can't really add any more useful context. + log.Error(r.Context(), err) + + // Any error returned here is unrecoverable, + // set Internal Server Error JSON response. + WriteResponseBytes(rw, r, + http.StatusInternalServerError, + AppJSON, + StatusInternalServerErrorJSON, + ) + } + + // Release. + putBuf(buf) +} + +// writeResponseUnknownLength handles reading data of unknown legnth +// efficiently into memory, and passing on to WriteResponseBytes(). +func writeResponseUnknownLength( + rw http.ResponseWriter, + r *http.Request, + statusCode int, + contentType string, + data io.Reader, +) { + // Acquire buffer. + buf := getBuf() + + // Read content into buffer. + _, err := buf.ReadFrom(data) + + if err == nil { + + // Respond with the now-known + // size byte slice within buf. + WriteResponseBytes(rw, r, + statusCode, + contentType, + buf.B, + ) + } else { + // This will always be a reader error (non EOF), + // but that doesn't mean the writer is closed yet! + log.Errorf(r.Context(), "error reading: %v", err) + + // Any error returned here is unrecoverable, + // set Internal Server Error JSON response. + WriteResponseBytes(rw, r, + http.StatusInternalServerError, + AppJSON, + StatusInternalServerErrorJSON, + ) + } + + // Release. + putBuf(buf) +} + +func getBuf() *byteutil.Buffer { + // acquire buffer from pool. + buf, _ := bufPool.Get().(*byteutil.Buffer) + + if buf == nil { + // alloc new buf if needed. + buf = new(byteutil.Buffer) + buf.B = make([]byte, 0, 4096) + } + + return buf +} + +func putBuf(buf *byteutil.Buffer) { + if cap(buf.B) >= int(^uint16(0)) { + // drop buffers of large size. + return + } + + // ensure empty. + buf.Reset() + + // release to pool. + bufPool.Put(buf) +} + +// mustJSON converts data to JSON, else panicking. +func mustJSON(data any) []byte { + b, err := json.Marshal(data) + if err != nil { + panic(err) + } + return b +} |