initial work on supporting the idempotency-key request header

This commit is contained in:
kim 2024-08-09 12:50:39 +01:00
parent 4a3ece0c6c
commit fd2be943b7
3 changed files with 122 additions and 3 deletions

View file

@ -441,17 +441,20 @@ func(context.Context, time.Time) {
gzip := middleware.Gzip() // applied to all except fileserver
// Idempotency applied only to client / AP.
idempotency := middleware.Idempotency()
// these should be routed in order;
// apply throttling *after* rate limiting
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)
healthModule.Route(route, clLimit, clThrottle)
fileserverModule.Route(route, fsMainLimit, fsThrottle)
fileserverModule.RouteEmojis(route, instanceAccount.ID, fsEmojiLimit, fsThrottle)
wellKnownModule.Route(route, gzip, s2sLimit, s2sThrottle)
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)
webModule.Route(route, fsMainLimit, fsThrottle, gzip)

View 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()
}
}

View file

@ -52,7 +52,7 @@ func RateLimit(limit int, exceptions []string) gin.HandlerFunc {
if limit <= 0 {
// Rate limiting is disabled.
// Return noop middleware.
return func(ctx *gin.Context) {}
return func(c *gin.Context) {}
}
limiter := limiter.New(