优化
This commit is contained in:
@@ -24,6 +24,9 @@ import (
|
||||
func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
if err := engine.SetTrustedProxies(nil); err != nil && logger != nil {
|
||||
logger.Error("disable trusted proxies failed", "error", err)
|
||||
}
|
||||
engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(logger), servermiddleware.AccessLog(runtime, logger, version), servermiddleware.CORS(runtime), servermiddleware.ErrorAudit(logger), servermiddleware.SecurityRateLimit(security))
|
||||
|
||||
prefix := ""
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/handler"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func emptyHandlers() *handler.Set {
|
||||
@@ -99,6 +101,29 @@ func TestSwaggerUsesRootBasePathWithoutRouterPrefix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGinDoesNotTrustForwardedIPByDefault(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
engine.GET("/client-ip", func(c *gin.Context) { c.String(http.StatusOK, c.ClientIP()) })
|
||||
request := httptest.NewRequest(http.MethodGet, "/client-ip", nil)
|
||||
request.RemoteAddr = "203.0.113.10:4321"
|
||||
request.Header.Set("X-Forwarded-For", "198.51.100.20")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || response.Body.String() != "203.0.113.10" {
|
||||
t.Fatalf("client IP = %q, status=%d", response.Body.String(), response.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnouncementDataSourceRequiresAuthentication(t *testing.T) {
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/info/getInfoDataSource", nil))
|
||||
if response.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalStorageResponseHeaders(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for name, body := range map[string]string{"script.html": "<script>alert(1)</script>", "image.png": "png"} {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"kra/internal/server/httpx"
|
||||
@@ -15,13 +17,40 @@ type Media struct{ service *service.MediaService }
|
||||
|
||||
func NewMedia(service *service.MediaService) *Media { return &Media{service: service} }
|
||||
|
||||
func (h *Media) limitMultipartBody(c *gin.Context) int64 {
|
||||
limit := h.service.MediaConfig().EffectiveMaxFileSize()
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit+(1<<20))
|
||||
return limit
|
||||
}
|
||||
|
||||
func rejectMediaTooLarge(c *gin.Context, err error, message string) bool {
|
||||
var tooLarge *http.MaxBytesError
|
||||
if !errors.As(err, &tooLarge) {
|
||||
return false
|
||||
}
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: message})
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Media) Upload(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
header, err := c.FormFile("file")
|
||||
if claims == nil || err != nil {
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "接收文件失败")
|
||||
return
|
||||
}
|
||||
limit := h.limitMultipartBody(c)
|
||||
header, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
if rejectMediaTooLarge(c, err, "文件超过大小上限") {
|
||||
return
|
||||
}
|
||||
httpx.Fail(c, "接收文件失败")
|
||||
return
|
||||
}
|
||||
if header.Size > limit {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: "文件超过大小上限"})
|
||||
return
|
||||
}
|
||||
opened, err := header.Open()
|
||||
if err != nil {
|
||||
// Let the upload service surface this as the generic upload
|
||||
@@ -191,13 +220,33 @@ func (h *Media) InitUpload(c *gin.Context) {
|
||||
}
|
||||
func (h *Media) SaveChunk(c *gin.Context) {
|
||||
claims := servermiddleware.Claims(c)
|
||||
uploadID, _ := strconv.ParseUint(c.PostForm("uploadId"), 10, 64)
|
||||
index, _ := strconv.Atoi(c.PostForm("chunkIndex"))
|
||||
header, err := c.FormFile("chunk")
|
||||
if claims == nil || err != nil {
|
||||
if claims == nil {
|
||||
httpx.Fail(c, "接收分片失败")
|
||||
return
|
||||
}
|
||||
limit := h.limitMultipartBody(c)
|
||||
header, err := c.FormFile("chunk")
|
||||
if err != nil {
|
||||
if rejectMediaTooLarge(c, err, "分片超过大小上限") {
|
||||
return
|
||||
}
|
||||
httpx.Fail(c, "接收分片失败")
|
||||
return
|
||||
}
|
||||
if header.Size > limit {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, httpx.Response{Code: httpx.CodeError, Data: gin.H{}, Msg: "分片超过大小上限"})
|
||||
return
|
||||
}
|
||||
uploadID, parseErr := strconv.ParseUint(c.PostForm("uploadId"), 10, 64)
|
||||
if parseErr != nil || uploadID == 0 {
|
||||
httpx.Fail(c, "上传会话 ID 非法")
|
||||
return
|
||||
}
|
||||
index, parseErr := strconv.Atoi(c.PostForm("chunkIndex"))
|
||||
if parseErr != nil || index < 0 {
|
||||
httpx.Fail(c, "分片序号非法")
|
||||
return
|
||||
}
|
||||
opened, err := header.Open()
|
||||
if err != nil {
|
||||
httpx.Fail(c, "分片读取失败")
|
||||
|
||||
@@ -34,12 +34,20 @@ func (h *Public) captchaConfig() (int, int, int) {
|
||||
|
||||
func (h *Public) Captcha(c *gin.Context) {
|
||||
keyLong, width, height := h.captchaConfig()
|
||||
security, _ := h.settings.CurrentSecurity(c.Request.Context())
|
||||
security, err := h.settings.CurrentSecurity(c.Request.Context())
|
||||
if err != nil || security == nil {
|
||||
httpx.Fail(c, "安全服务暂不可用")
|
||||
return
|
||||
}
|
||||
openCaptcha := true
|
||||
if security != nil {
|
||||
keyLong, width, height = security.KeyLong, security.ImgWidth, security.ImgHeight
|
||||
ttl := time.Duration(security.CaptchaTimeout) * time.Second
|
||||
failures, _ := h.settings.EnsureLoginIPCounter(c.Request.Context(), c.ClientIP(), ttl)
|
||||
failures, counterErr := h.settings.EnsureLoginIPCounter(c.Request.Context(), c.ClientIP(), ttl)
|
||||
if counterErr != nil {
|
||||
httpx.Fail(c, "安全服务暂不可用")
|
||||
return
|
||||
}
|
||||
openCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen
|
||||
}
|
||||
driver := base64Captcha.NewDriverDigit(height, width, keyLong, 0.7, 80)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/httpx"
|
||||
@@ -9,7 +11,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AccessControl(runtime *conf.Runtime, access *service.AccessControlService) gin.HandlerFunc {
|
||||
type accessController interface {
|
||||
Authorize(context.Context, uint, string, string) (bool, error)
|
||||
ContextWithDataScope(context.Context, uint, uint) (context.Context, error)
|
||||
}
|
||||
|
||||
func AccessControl(runtime *conf.Runtime, access accessController) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims := Claims(c)
|
||||
if claims == nil {
|
||||
@@ -29,7 +36,9 @@ func AccessControl(runtime *conf.Runtime, access *service.AccessControlService)
|
||||
}
|
||||
requestContext, err := access.ContextWithDataScope(c.Request.Context(), claims.AuthorityID, claims.ID)
|
||||
if err != nil {
|
||||
requestContext = c.Request.Context()
|
||||
httpx.Write(c, httpx.CodeError, gin.H{}, "数据权限解析失败")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if scope, ok := biz.DataScopeFromContext(requestContext); ok {
|
||||
requestID, _ := c.Get("request_id")
|
||||
|
||||
@@ -2,17 +2,22 @@ package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/httpx"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const defaultRequestBodyLimit int64 = 8 << 20
|
||||
|
||||
// AccessLog is the single global request/response capture point, matching
|
||||
// the reference middleware ordering and making every HTTP request observable.
|
||||
func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.HandlerFunc {
|
||||
@@ -20,20 +25,45 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.H
|
||||
started := time.Now()
|
||||
var requestBody []byte
|
||||
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
|
||||
mediaUpload := multipart && isMediaUploadRoute(c.FullPath())
|
||||
config := runtime.Admin()
|
||||
bodyLimit := defaultRequestBodyLimit
|
||||
if mediaUpload {
|
||||
bodyLimit = biz.DefaultMaxMediaFileSize + (1 << 20)
|
||||
if config != nil && config.Media != nil && config.Media.MaxFileSize > 0 {
|
||||
bodyLimit = config.Media.MaxFileSize + (1 << 20)
|
||||
}
|
||||
}
|
||||
bytesIn := c.Request.ContentLength
|
||||
if c.Request.Body != nil && !multipart {
|
||||
requestBody, _ = io.ReadAll(c.Request.Body)
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
bytesIn = int64(len(requestBody))
|
||||
}
|
||||
if bytesIn < 0 {
|
||||
bytesIn = 0
|
||||
}
|
||||
maxBytes := 1 << 20
|
||||
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
|
||||
c.Writer = writer
|
||||
c.Header("X-Kra-Version", version)
|
||||
config := runtime.Admin()
|
||||
requestReadFailed := c.Request.ContentLength > bodyLimit
|
||||
if requestReadFailed {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
|
||||
} else if c.Request.Body != nil && !mediaUpload {
|
||||
limited := http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
|
||||
var err error
|
||||
requestBody, err = io.ReadAll(limited)
|
||||
if err != nil {
|
||||
requestReadFailed = true
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"code": httpx.CodeError, "msg": "请求体超过大小上限"})
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": httpx.CodeError, "msg": "请求体读取失败"})
|
||||
}
|
||||
} else {
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
bytesIn = int64(len(requestBody))
|
||||
}
|
||||
} else if c.Request.Body != nil {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, bodyLimit)
|
||||
}
|
||||
if bytesIn < 0 {
|
||||
bytesIn = 0
|
||||
}
|
||||
logLimit := 1024
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
|
||||
logLimit = int(config.Zap.AccessLogMaxBytes)
|
||||
@@ -46,7 +76,9 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.H
|
||||
}
|
||||
c.Set(ctxReqBodyKey, requestText)
|
||||
c.Set(ctxRespBufferKey, &writer.body)
|
||||
c.Next()
|
||||
if !requestReadFailed {
|
||||
c.Next()
|
||||
}
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
@@ -86,6 +118,11 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.H
|
||||
}
|
||||
}
|
||||
|
||||
func isMediaUploadRoute(route string) bool {
|
||||
return strings.HasSuffix(route, "/fileUploadAndDownload/upload") ||
|
||||
strings.HasSuffix(route, "/mediaUpload/chunk")
|
||||
}
|
||||
|
||||
func redactHeaders(headers map[string][]string) map[string]string {
|
||||
out := make(map[string]string, len(headers))
|
||||
for key, values := range headers {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/conf"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestAccessLogRejectsOversizedRequestBody(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
|
||||
engine.POST("/payload", func(c *gin.Context) { called = true })
|
||||
request := httptest.NewRequest(http.MethodPost, "/payload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusRequestEntityTooLarge || called {
|
||||
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessLogRejectsOversizedMultipartOnOrdinaryRoute(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
|
||||
engine.POST("/login", func(c *gin.Context) { called = true })
|
||||
request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
|
||||
request.ContentLength = -1
|
||||
request.Header.Set("Content-Type", "multipart/form-data; boundary=test")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusRequestEntityTooLarge || called {
|
||||
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccessLogAllowsMediaLimitOnlyOnUploadRoute(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(AccessLog(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, "test"))
|
||||
engine.POST("/api/fileUploadAndDownload/upload", func(c *gin.Context) {
|
||||
called = true
|
||||
_, _ = io.Copy(io.Discard, c.Request.Body)
|
||||
})
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/fileUploadAndDownload/upload", strings.NewReader(strings.Repeat("a", int(defaultRequestBodyLimit+1))))
|
||||
request.Header.Set("Content-Type", "multipart/form-data; boundary=test")
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
engine.ServeHTTP(response, request)
|
||||
if response.Code != http.StatusOK || !called {
|
||||
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type accessControllerStub struct {
|
||||
scopeErr error
|
||||
}
|
||||
|
||||
func (*accessControllerStub) Authorize(context.Context, uint, string, string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *accessControllerStub) ContextWithDataScope(ctx context.Context, authorityID, userID uint) (context.Context, error) {
|
||||
if s.scopeErr != nil {
|
||||
return ctx, s.scopeErr
|
||||
}
|
||||
return biz.NewDataScopeContext(ctx, biz.DataScope{UserID: userID, AuthorityID: authorityID, Scope: 1, All: true}), nil
|
||||
}
|
||||
|
||||
func TestAccessControlFailsClosedWhenDataScopeResolutionFails(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set(claimsKey, &biz.AuthClaims{ID: 7, AuthorityID: 888})
|
||||
c.Next()
|
||||
})
|
||||
engine.Use(AccessControl(conf.NewRuntime(nil, &conf.AdminBackend{}), &accessControllerStub{scopeErr: errors.New("database unavailable")}))
|
||||
called := false
|
||||
engine.GET("/protected", func(c *gin.Context) { called = true })
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/protected", nil))
|
||||
if called {
|
||||
t.Fatal("protected handler ran after data-scope resolution failed")
|
||||
}
|
||||
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "数据权限解析失败") {
|
||||
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package middleware
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -41,7 +42,18 @@ func OperationAudit(runtime *conf.Runtime, service *service.AuditRecorder) gin.H
|
||||
if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
|
||||
requestBody = []byte("[文件]")
|
||||
} else {
|
||||
requestBody, _ = io.ReadAll(c.Request.Body)
|
||||
limited := http.MaxBytesReader(c.Writer, c.Request.Body, defaultRequestBodyLimit)
|
||||
var err error
|
||||
requestBody, err = io.ReadAll(limited)
|
||||
if err != nil {
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
c.AbortWithStatus(http.StatusRequestEntityTooLarge)
|
||||
} else {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -18,7 +19,11 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
config, err := settings.CurrentSecurity(c.Request.Context())
|
||||
if err != nil || config == nil || !config.LimitEnable {
|
||||
if err != nil || config == nil {
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
|
||||
return
|
||||
}
|
||||
if !config.LimitEnable {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -28,7 +33,11 @@ func SecurityRateLimit(settings *service.SecurityService) gin.HandlerFunc {
|
||||
}
|
||||
key := "KRA_SecLimit" + c.ClientIP() + c.FullPath()
|
||||
count, cacheErr := settings.IncrementRateLimit(c.Request.Context(), key, time.Duration(window)*time.Second)
|
||||
if cacheErr == nil && int(count) > config.LimitCount {
|
||||
if cacheErr != nil {
|
||||
c.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"code": httpx.CodeError, "msg": "安全服务暂不可用"})
|
||||
return
|
||||
}
|
||||
if int(count) > config.LimitCount {
|
||||
c.JSON(200, gin.H{"code": httpx.CodeError, "msg": "请求太过频繁,请稍后再试"})
|
||||
c.Abort()
|
||||
return
|
||||
|
||||
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -26,12 +27,14 @@ func (rateLimitSecurityRepo) BackfillPasswordUpdatedAt(context.Context, time.Tim
|
||||
return nil
|
||||
}
|
||||
|
||||
type rateLimitCache struct{}
|
||||
type rateLimitCache struct{ err error }
|
||||
|
||||
func (rateLimitCache) Get(context.Context, string) (string, bool, error) { return "", false, nil }
|
||||
func (rateLimitCache) Set(context.Context, string, string, time.Duration) error { return nil }
|
||||
func (rateLimitCache) Delete(context.Context, string) error { return nil }
|
||||
func (rateLimitCache) Increment(context.Context, string, time.Duration) (int64, error) { return 2, nil }
|
||||
func (rateLimitCache) Get(context.Context, string) (string, bool, error) { return "", false, nil }
|
||||
func (rateLimitCache) Set(context.Context, string, string, time.Duration) error { return nil }
|
||||
func (rateLimitCache) Delete(context.Context, string) error { return nil }
|
||||
func (c rateLimitCache) Increment(context.Context, string, time.Duration) (int64, error) {
|
||||
return 2, c.err
|
||||
}
|
||||
|
||||
func TestSecurityRateLimitMatchesResponseContract(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -50,3 +53,18 @@ func TestSecurityRateLimitMatchesResponseContract(t *testing.T) {
|
||||
t.Fatalf("unexpected rate-limit response: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityRateLimitFailsClosedWhenCacheIsUnavailable(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
settings := service.NewSecurityService(biz.NewSecurityUsecase(rateLimitSecurityRepo{}, rateLimitCache{err: errors.New("cache unavailable")}, nil, nil))
|
||||
engine := gin.New()
|
||||
called := false
|
||||
engine.Use(SecurityRateLimit(settings))
|
||||
engine.POST("/base/login", func(c *gin.Context) { called = true })
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
engine.ServeHTTP(response, httptest.NewRequest(http.MethodPost, "/base/login", nil))
|
||||
if response.Code != http.StatusServiceUnavailable || called {
|
||||
t.Fatalf("status=%d called=%v body=%s", response.Code, called, response.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func RegisterAnnouncement(private, public *gin.RouterGroup, h *handler.Announcem
|
||||
privateInfo.PUT("/updateInfo", h.Update)
|
||||
privateInfo.GET("/findInfo", h.Find)
|
||||
privateInfo.GET("/getInfoList", h.List)
|
||||
privateInfo.GET("/getInfoDataSource", h.DataSource)
|
||||
publicInfo := public.Group("/info")
|
||||
publicInfo.GET("/getInfoDataSource", h.DataSource)
|
||||
publicInfo.GET("/getInfoPublic", h.Public)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user