mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-02-05 15:47:47 +01:00
initial work on supporting the idempotency-key request header
This commit is contained in:
parent
4a3ece0c6c
commit
fd2be943b7
3 changed files with 122 additions and 3 deletions
|
@ -441,17 +441,20 @@ func(context.Context, time.Time) {
|
||||||
|
|
||||||
gzip := middleware.Gzip() // applied to all except fileserver
|
gzip := middleware.Gzip() // applied to all except fileserver
|
||||||
|
|
||||||
|
// Idempotency applied only to client / AP.
|
||||||
|
idempotency := middleware.Idempotency()
|
||||||
|
|
||||||
// these should be routed in order;
|
// these should be routed in order;
|
||||||
// apply throttling *after* rate limiting
|
// apply throttling *after* rate limiting
|
||||||
authModule.Route(route, clLimit, clThrottle, gzip)
|
authModule.Route(route, clLimit, clThrottle, gzip)
|
||||||
clientModule.Route(route, clLimit, clThrottle, gzip)
|
clientModule.Route(route, clLimit, clThrottle, idempotency, gzip)
|
||||||
metricsModule.Route(route, clLimit, clThrottle, gzip)
|
metricsModule.Route(route, clLimit, clThrottle, gzip)
|
||||||
healthModule.Route(route, clLimit, clThrottle)
|
healthModule.Route(route, clLimit, clThrottle)
|
||||||
fileserverModule.Route(route, fsMainLimit, fsThrottle)
|
fileserverModule.Route(route, fsMainLimit, fsThrottle)
|
||||||
fileserverModule.RouteEmojis(route, instanceAccount.ID, fsEmojiLimit, fsThrottle)
|
fileserverModule.RouteEmojis(route, instanceAccount.ID, fsEmojiLimit, fsThrottle)
|
||||||
wellKnownModule.Route(route, gzip, s2sLimit, s2sThrottle)
|
wellKnownModule.Route(route, gzip, s2sLimit, s2sThrottle)
|
||||||
nodeInfoModule.Route(route, s2sLimit, s2sThrottle, gzip)
|
nodeInfoModule.Route(route, s2sLimit, s2sThrottle, gzip)
|
||||||
activityPubModule.Route(route, s2sLimit, s2sThrottle, gzip)
|
activityPubModule.Route(route, s2sLimit, s2sThrottle, idempotency, gzip)
|
||||||
activityPubModule.RoutePublicKey(route, s2sLimit, pkThrottle, gzip)
|
activityPubModule.RoutePublicKey(route, s2sLimit, pkThrottle, gzip)
|
||||||
webModule.Route(route, fsMainLimit, fsThrottle, gzip)
|
webModule.Route(route, fsMainLimit, fsThrottle, gzip)
|
||||||
|
|
||||||
|
|
116
internal/middleware/idempotency.go
Normal file
116
internal/middleware/idempotency.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// 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 middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/gruf/go-cache/v3/ttl"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Idempotency returns a piece of gin middleware
|
||||||
|
// capable of handling the Idempotency-Key header:
|
||||||
|
// https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/
|
||||||
|
func Idempotency() gin.HandlerFunc {
|
||||||
|
|
||||||
|
// Prepare expected error response JSON ahead of time.
|
||||||
|
errorConflict, err := json.Marshal(map[string]string{
|
||||||
|
"error": "request already under way",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare an idempotency responses cache for responses.
|
||||||
|
responses := ttl.New[string, int](0, 1000, 5*time.Minute)
|
||||||
|
if !responses.Start(time.Minute) {
|
||||||
|
panic("failed to start idempotency cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Ignore requests that don't provide
|
||||||
|
// a body, i.e. generally will not be
|
||||||
|
// updating any server resources.
|
||||||
|
switch c.Request.Method {
|
||||||
|
case "HEAD", "GET":
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for idempotency key provided in header.
|
||||||
|
key := c.Request.Header.Get("Idempotency-Key")
|
||||||
|
if key == "" {
|
||||||
|
|
||||||
|
// When no key is
|
||||||
|
// provided, just
|
||||||
|
// skip the rest of
|
||||||
|
// this middleware.
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the key we use to include general
|
||||||
|
// request fingerprint along with idempotency
|
||||||
|
// key to ensure uniqueness across logged-in
|
||||||
|
// device sessions regardless of IP.
|
||||||
|
key = c.Request.Header.Get("Authorization") +
|
||||||
|
c.Request.UserAgent() + key
|
||||||
|
|
||||||
|
// Look for stored response.
|
||||||
|
code, _ := responses.Get(key)
|
||||||
|
switch code {
|
||||||
|
|
||||||
|
// Not yet
|
||||||
|
// handled.
|
||||||
|
case 0:
|
||||||
|
|
||||||
|
// Request is already
|
||||||
|
// under way for key.
|
||||||
|
case -1:
|
||||||
|
apiutil.Data(c,
|
||||||
|
http.StatusConflict,
|
||||||
|
apiutil.AppJSON,
|
||||||
|
errorConflict,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Already handled
|
||||||
|
// this request.
|
||||||
|
default:
|
||||||
|
c.Status(code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if code := c.Writer.Status(); code != 0 {
|
||||||
|
// Store response in map,
|
||||||
|
// codes of zero indicate
|
||||||
|
// a panic during handling.
|
||||||
|
responses.Set(key, code)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Pass on to next
|
||||||
|
// handler in chain.
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ func RateLimit(limit int, exceptions []string) gin.HandlerFunc {
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
// Rate limiting is disabled.
|
// Rate limiting is disabled.
|
||||||
// Return noop middleware.
|
// Return noop middleware.
|
||||||
return func(ctx *gin.Context) {}
|
return func(c *gin.Context) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
limiter := limiter.New(
|
limiter := limiter.New(
|
||||||
|
|
Loading…
Reference in a new issue