mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
API: Export request limit helpers and align test guidance
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
cc7064b5a9
commit
9219d14d3f
47 changed files with 485 additions and 56 deletions
|
|
@ -225,6 +225,7 @@ Note: Across our public documentation, official images, and in production, the c
|
|||
- Test frontend/backend: `make test-js` and `make test-go`
|
||||
- Linting: `make lint` (all), `make lint-go` (golangci-lint with `.golangci.yml`, prints findings without failing due to `--issues-exit-code 0`), `make lint-js` (ESLint/Prettier)
|
||||
- Go packages: `go test` (all tests) or `go test -run <name>` (specific tests only)
|
||||
- Do not run multiple test commands in parallel. Suites share fixture files, temporary assets, and database state, so concurrent runs can trigger false failures, readonly database errors, or fixture conflicts.
|
||||
- Need to inspect the MariaDB data while iterating? Connect directly inside the dev shell with `mariadb -D photoprism` and run SQL without rebuilding Go code.
|
||||
- Go tests live beside sources: for `path/to/pkg/<file>.go`, add tests in `path/to/pkg/<file>_test.go` (create if missing). For the same function, group related cases as `t.Run(...)` sub-tests (table-driven where helpful) and use **PascalCase** for subtest names (for example, `t.Run("Success", ...)`).
|
||||
- Frontend unit tests use **Vitest**; see scripts in `frontend/package.json`.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each fil
|
|||
### Security & Middleware
|
||||
|
||||
- Authenticate requests using the standard middleware (`AuthRequired`) and check roles via helpers in `internal/auth/acl` (`acl.ParseRole`, `acl.ScopePermits`, `acl.ScopeAttrPermits`).
|
||||
- Bound request bodies before parsing JSON or multipart payloads. Use `LimitRequestBodyBytes(...)` with a route-appropriate cap before `BindJSON(...)` / `ShouldBindJSON(...)`, detect `IsRequestBodyTooLarge(err)`, and return `413 Request Entity Too Large` via `AbortRequestTooLarge(...)`.
|
||||
- Never log secrets or tokens. Prefer structured logging through `event.Log` and redact sensitive values before logging.
|
||||
- Enforce rate limiting with the shared limiters (`limiter.Auth`, `limiter.Login`) and respond with `limiter.AbortJSON` to maintain consistent 429 JSON payloads.
|
||||
- Derive client IPs through `api.ClientIP` and extract bearer tokens with `header.BearerToken` or the helper setters. Use constant-time comparison for tokens and secrets.
|
||||
|
|
@ -70,6 +71,7 @@ The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each fil
|
|||
- When handlers interact with the database, initialize fixtures through config helpers such as `config.NewTestConfig("api")` or `config.NewMinimalTestConfigWithDb("api", t.TempDir())` depending on fixture needs.
|
||||
- Stub external dependencies (`httptest.Server`) for remote calls and set `AllowPrivate=true` explicitly when the test server binds to loopback addresses.
|
||||
- Structure tests with table-driven subtests (`t.Run("CaseName", ...)`) and use PascalCase names. Provide cleanup functions (`t.Cleanup`) to remove temporary files or databases created during tests.
|
||||
- Do not run `internal/api` tests in parallel. These suites share fixture files, temporary assets, and database state, so parallel `go test` invocations can cause false failures and readonly/fixture-conflict errors.
|
||||
|
||||
### Focused Test Runs
|
||||
|
||||
|
|
@ -77,6 +79,7 @@ The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each fil
|
|||
- Cluster endpoints: `go test ./internal/api -run 'Cluster' -count=1`
|
||||
- Downloads and zip streaming: `go test ./internal/api -run 'Download|Archive' -count=1`
|
||||
- Combined CLI and API validation: pair `go test ./internal/commands -run 'Cluster' -count=1` with the matching API suite to ensure DTOs remain compatible.
|
||||
- Keep focused `internal/api` runs sequential. Do not launch multiple `go test ./internal/api ...` commands at the same time.
|
||||
|
||||
### Preflight Checklist
|
||||
|
||||
|
|
|
|||
|
|
@ -138,11 +138,11 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||
var frm form.Album
|
||||
|
||||
// Assign and validate request form values.
|
||||
limitRequestBodyBytes(c, maxAlbumRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxAlbumRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -245,11 +245,11 @@ func UpdateAlbum(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Assign and validate request form values.
|
||||
limitRequestBodyBytes(c, maxAlbumRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxAlbumRequestBytes)
|
||||
|
||||
if err = c.BindJSON(frm); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -501,11 +501,11 @@ func CloneAlbums(router *gin.RouterGroup) {
|
|||
var frm form.Selection
|
||||
|
||||
// Assign and validate request form values.
|
||||
limitRequestBodyBytes(c, maxAlbumRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxAlbumRequestBytes)
|
||||
|
||||
if err = c.BindJSON(&frm); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -569,11 +569,11 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
|
|||
var frm form.Selection
|
||||
|
||||
// Assign and validate request form values.
|
||||
limitRequestBodyBytes(c, maxAlbumRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxAlbumRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -684,11 +684,11 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
|
|||
var frm form.Selection
|
||||
|
||||
// Assign and validate request form values.
|
||||
limitRequestBodyBytes(c, maxAlbumRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxAlbumRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,14 @@ func BatchAlbumsDelete(router *gin.RouterGroup) {
|
|||
|
||||
var frm form.Selection
|
||||
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,14 @@ func BatchLabelsDelete(router *gin.RouterGroup) {
|
|||
|
||||
var frm form.Selection
|
||||
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,14 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
|
|||
var frm form.Selection
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -115,7 +122,14 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
|
|||
|
||||
var frm form.Selection
|
||||
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -185,7 +199,14 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
|
|||
|
||||
var frm form.Selection
|
||||
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -245,7 +266,14 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
|
|||
|
||||
var frm form.Selection
|
||||
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -312,7 +340,14 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
|
|||
|
||||
var frm form.Selection
|
||||
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ func BatchPhotosEdit(router *gin.RouterGroup) {
|
|||
var frm batch.PhotosRequest
|
||||
|
||||
// Assign and validate request form values.
|
||||
limitRequestBodyBytes(c, maxBatchPhotosEditBytes)
|
||||
LimitRequestBodyBytes(c, MaxBatchPhotosEditBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -46,6 +47,15 @@ func TestBatchPhotosArchive(t *testing.T) {
|
|||
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/archive", `{"photos": 123}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
BatchPhotosArchive(router)
|
||||
|
||||
body := `{"photos":["` + strings.Repeat("p", int(MaxSelectionRequestBytes)) + `"]}`
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/batch/photos/archive", body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBatchPhotosRestore(t *testing.T) {
|
||||
|
|
@ -135,6 +145,15 @@ func TestBatchAlbumsDelete(t *testing.T) {
|
|||
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/albums/delete", `{"albums": 123}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
BatchAlbumsDelete(router)
|
||||
|
||||
body := `{"albums":["` + strings.Repeat("a", int(MaxSelectionRequestBytes)) + `"]}`
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/batch/albums/delete", body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBatchPhotosPrivate(t *testing.T) {
|
||||
|
|
@ -220,6 +239,15 @@ func TestBatchLabelsDelete(t *testing.T) {
|
|||
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/labels/delete", `{"labels": 123}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
BatchLabelsDelete(router)
|
||||
|
||||
body := `{"labels":["` + strings.Repeat("l", int(MaxSelectionRequestBytes)) + `"]}`
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/batch/labels/delete", body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBatchPhotosApprove(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/internal/service/cluster"
|
||||
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
|
@ -222,7 +223,14 @@ func ClusterUpdateNode(router *gin.RouterGroup) {
|
|||
SiteUrl *string `json:"SiteUrl"`
|
||||
}
|
||||
|
||||
LimitRequestBodyBytes(c, MaxClusterRegisterBytes)
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,12 +79,12 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
|||
// Parse request.
|
||||
var req cluster.RegisterRequest
|
||||
|
||||
limitRequestBodyBytes(c, maxClusterRegisterBytes)
|
||||
LimitRequestBodyBytes(c, MaxClusterRegisterBytes)
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "request too large", status.Error(err)})
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -140,3 +141,21 @@ func TestClusterUpdateNode_UUIDValidation(t *testing.T) {
|
|||
r = PerformRequestWithBody(app, http.MethodPatch, "/api/v1/cluster/nodes/BadID", `{"SiteUrl":"https://photos.example.com"}`)
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
}
|
||||
|
||||
func TestClusterUpdateNode_RequestTooLarge(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
enablePortalAPIs(t, conf)
|
||||
|
||||
ClusterUpdateNode(router)
|
||||
|
||||
regy, err := reg.NewClientRegistryWithConfig(conf)
|
||||
assert.NoError(t, err)
|
||||
|
||||
n := ®.Node{Node: cluster.Node{Name: "pp-node-request-limit", Role: cluster.RoleInstance, UUID: rnd.UUIDv7()}}
|
||||
assert.NoError(t, regy.Put(n))
|
||||
|
||||
body := `{"Labels":{"oversized":"` + strings.Repeat("a", int(MaxClusterRegisterBytes)) + `"}}`
|
||||
r := PerformRequestWithBody(app, http.MethodPatch, "/api/v1/cluster/nodes/"+n.UUID, body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
)
|
||||
|
||||
// GetConfigOptions returns backend config options.
|
||||
|
|
@ -58,7 +59,14 @@ func SaveConfigOptions(router *gin.RouterGroup) {
|
|||
|
||||
v := make(entity.Values)
|
||||
|
||||
LimitRequestBodyBytes(c, MaxSettingsRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&v); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
|
@ -101,6 +102,19 @@ func TestSaveConfigOptions(t *testing.T) {
|
|||
assert.Equal(t, "https://photos.example.com/", merged["SiteUrl"])
|
||||
assert.Equal(t, true, merged["HttpCachePublic"])
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
|
||||
SaveConfigOptions(router)
|
||||
|
||||
prepareConfigOptionsSuccessTest(t, conf)
|
||||
|
||||
authToken := AuthenticateAdmin(app, router)
|
||||
body := `{"SiteUrl":"https://photos.example.com/","LogLevel":"` + strings.Repeat("a", int(MaxSettingsRequestBytes)) + `"}`
|
||||
r := AuthenticatedRequestWithBody(app, "POST", "/api/v1/config/options", body, authToken)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// prepareConfigOptionsSuccessTest normalizes shared config state so the success
|
||||
|
|
|
|||
|
|
@ -85,11 +85,11 @@ func SaveSettings(router *gin.RouterGroup) {
|
|||
settings = conf.Settings()
|
||||
|
||||
// Set values from request.
|
||||
limitRequestBodyBytes(c, maxSettingsRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxSettingsRequestBytes)
|
||||
|
||||
if err := c.BindJSON(settings); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -119,11 +119,11 @@ func SaveSettings(router *gin.RouterGroup) {
|
|||
settings = &customize.Settings{}
|
||||
|
||||
// Set values from request.
|
||||
limitRequestBodyBytes(c, maxSettingsRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxSettingsRequestBytes)
|
||||
|
||||
if err := c.BindJSON(settings); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,15 @@ func Connect(router *gin.RouterGroup) {
|
|||
var frm form.Connect
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxAuthRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
log.Warnf("connect: request too large (%s)", clean.Log(name))
|
||||
AbortRequestTooLarge(c, i18n.ErrAccountConnect)
|
||||
return
|
||||
}
|
||||
|
||||
log.Warnf("connect: invalid form values (%s)", clean.Log(name))
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrAccountConnect)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -43,4 +45,13 @@ func TestConnect(t *testing.T) {
|
|||
r := PerformRequestWithBody(app, "PUT", "/api/v1/connect/hub/", `{"Token": "foobar123"}`)
|
||||
assert.NotEqual(t, 200, r.Code)
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
Connect(router)
|
||||
|
||||
body := `{"Token":"` + strings.Repeat("a", int(MaxAuthRequestBytes)) + `"}`
|
||||
r := PerformRequestWithBody(app, http.MethodPut, "/api/v1/connect/hub", body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,14 @@ func UpdateFace(router *gin.RouterGroup) {
|
|||
var frm form.Face
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,14 @@ func ChangeFileOrientation(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err = c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,14 @@ func StartImport(router *gin.RouterGroup) {
|
|||
var frm form.ImportOptions
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,14 @@ func StartIndexing(router *gin.RouterGroup) {
|
|||
var frm form.IndexOptions
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,14 @@ func UpdateLabel(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Set form values from request.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if frmErr = c.BindJSON(frm); frmErr != nil {
|
||||
if IsRequestBodyTooLarge(frmErr) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, frmErr)
|
||||
return
|
||||
} else if frmErr = frm.Validate(); frmErr != nil {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/entity/query"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
|
|
@ -28,7 +29,14 @@ func UpdateLink(c *gin.Context) {
|
|||
var frm form.Link
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -107,7 +115,14 @@ func CreateLink(c *gin.Context) {
|
|||
|
||||
var frm form.Link
|
||||
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,14 @@ func CreateMarker(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Initialize form.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -214,7 +221,16 @@ func UpdateMarker(router *gin.RouterGroup) {
|
|||
log.Errorf("faces: %s (create marker form)", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
} else if err = c.BindJSON(&frm); err != nil {
|
||||
}
|
||||
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err = c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("faces: %s (bind marker form)", err)
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
|
|
@ -47,7 +48,14 @@ func AddPhotoLabel(router *gin.RouterGroup) {
|
|||
frm := &form.Label{}
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err = c.BindJSON(frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
} else if err = frm.Validate(); err != nil {
|
||||
|
|
@ -231,7 +239,14 @@ func UpdatePhotoLabel(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err = c.BindJSON(label); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,14 @@ func UpdatePhoto(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// 2) Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err = c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,27 +11,43 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
maxSessionRequestBytes int64 = 64 * 1024
|
||||
maxAlbumRequestBytes int64 = 256 * 1024
|
||||
maxSettingsRequestBytes int64 = 256 * 1024
|
||||
maxClusterRegisterBytes int64 = 256 * 1024
|
||||
maxUploadOptionsRequestBytes int64 = 256 * 1024
|
||||
maxBatchPhotosEditBytes int64 = 1024 * 1024
|
||||
maxMultipartOverheadBytes int64 = 1024 * 1024
|
||||
maxAvatarUploadBytes int64 = 20000000 + maxMultipartOverheadBytes
|
||||
// MaxAuthRequestBytes bounds small authentication and credential-change payloads.
|
||||
MaxAuthRequestBytes int64 = 64 * 1024
|
||||
// MaxMutationRequestBytes bounds general JSON mutation payloads.
|
||||
MaxMutationRequestBytes int64 = 256 * 1024
|
||||
// MaxSelectionRequestBytes bounds selection-heavy batch mutation payloads.
|
||||
MaxSelectionRequestBytes int64 = 1024 * 1024
|
||||
// MaxVisionRequestBytes bounds Vision API payloads while still allowing supported data URLs.
|
||||
MaxVisionRequestBytes int64 = 32 * 1024 * 1024
|
||||
// MaxSessionRequestBytes bounds login request payloads.
|
||||
MaxSessionRequestBytes int64 = MaxAuthRequestBytes
|
||||
// MaxAlbumRequestBytes bounds album create and update payloads.
|
||||
MaxAlbumRequestBytes int64 = MaxMutationRequestBytes
|
||||
// MaxSettingsRequestBytes bounds settings and config option payloads.
|
||||
MaxSettingsRequestBytes int64 = MaxMutationRequestBytes
|
||||
// MaxClusterRegisterBytes bounds cluster registration and patch payloads.
|
||||
MaxClusterRegisterBytes int64 = MaxMutationRequestBytes
|
||||
// MaxUploadOptionsRequestBytes bounds upload-processing payloads.
|
||||
MaxUploadOptionsRequestBytes int64 = MaxMutationRequestBytes
|
||||
// MaxBatchPhotosEditBytes bounds batch photo edit payloads.
|
||||
MaxBatchPhotosEditBytes int64 = MaxSelectionRequestBytes
|
||||
// MaxMultipartOverheadBytes reserves room for multipart framing overhead.
|
||||
MaxMultipartOverheadBytes int64 = 1024 * 1024
|
||||
// MaxAvatarUploadBytes bounds avatar uploads including multipart overhead.
|
||||
MaxAvatarUploadBytes int64 = 20000000 + MaxMultipartOverheadBytes
|
||||
)
|
||||
|
||||
// limitRequestBodyBytes caps the readable request body size for the current handler.
|
||||
func limitRequestBodyBytes(c *gin.Context, limit int64) {
|
||||
if c == nil || c.Request == nil || limit <= 0 {
|
||||
// LimitRequestBodyBytes caps the readable request body size for the current handler.
|
||||
func LimitRequestBodyBytes(c *gin.Context, limit int64) {
|
||||
if c == nil || c.Request == nil || c.Request.Body == nil || limit <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, limit)
|
||||
}
|
||||
|
||||
// isRequestBodyTooLarge reports whether the parsing error was caused by a body-size limit.
|
||||
func isRequestBodyTooLarge(err error) bool {
|
||||
// IsRequestBodyTooLarge reports whether the parsing error was caused by a body-size limit.
|
||||
func IsRequestBodyTooLarge(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
|
@ -41,7 +57,7 @@ func isRequestBodyTooLarge(err error) bool {
|
|||
return errors.As(err, &maxBytesErr) || errors.Is(err, multipart.ErrMessageTooLarge)
|
||||
}
|
||||
|
||||
// abortRequestTooLarge writes a localized 413 response and stops the current request.
|
||||
func abortRequestTooLarge(c *gin.Context, id i18n.Message) {
|
||||
// AbortRequestTooLarge writes a localized 413 response and stops the current request.
|
||||
func AbortRequestTooLarge(c *gin.Context, id i18n.Message) {
|
||||
Abort(c, http.StatusRequestEntityTooLarge, id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,14 @@ func AddService(router *gin.RouterGroup) {
|
|||
var frm form.Service
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
@ -220,7 +227,14 @@ func UpdateService(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// 2) Update form with values from request
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err = c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,14 @@ func UploadToService(router *gin.RouterGroup) {
|
|||
var frm form.SyncUpload
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err = c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,12 +41,12 @@ func CreateSession(router *gin.RouterGroup) {
|
|||
clientIp := ClientIP(c)
|
||||
|
||||
// Assign and validate request form values.
|
||||
limitRequestBodyBytes(c, maxSessionRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxSessionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
event.AuditWarn([]string{clientIp, "create session", "request too large", status.Error(err)})
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,16 @@ func UpdateSubject(router *gin.RouterGroup) {
|
|||
log.Errorf("subject: %s (new form)", err)
|
||||
AbortSaveFailed(c)
|
||||
return
|
||||
} else if err = c.BindJSON(frm); err != nil {
|
||||
}
|
||||
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err = c.BindJSON(frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorf("subject: %s (update form)", err)
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -58,14 +58,14 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Parse upload form.
|
||||
limitRequestBodyBytes(c, maxAvatarUploadBytes)
|
||||
LimitRequestBodyBytes(c, MaxAvatarUploadBytes)
|
||||
|
||||
f, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", "upload avatar", "request too large"}, s.RefID)
|
||||
abortRequestTooLarge(c, i18n.ErrFileTooLarge)
|
||||
AbortRequestTooLarge(c, i18n.ErrFileTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -292,7 +292,14 @@ func checkUserPasscodeAuth(c *gin.Context, action acl.Permission) (*entity.Sessi
|
|||
frm := &form.Passcode{}
|
||||
|
||||
// Validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxAuthRequestBytes)
|
||||
|
||||
if err := c.BindJSON(frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrInvalidPassword)
|
||||
return s, nil, nil, authn.ErrInvalidRequest
|
||||
}
|
||||
|
||||
Error(c, http.StatusBadRequest, err, i18n.ErrInvalidPassword)
|
||||
return s, nil, nil, authn.ErrInvalidRequest
|
||||
} else if authn.KeyTOTP.NotEqual(frm.Type) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -87,6 +88,18 @@ func TestCreateUserPasscode(t *testing.T) {
|
|||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
}
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
CreateUserPasscode(router)
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
body := `{"Type":"totp","Password":"` + strings.Repeat("a", int(MaxAuthRequestBytes)) + `"}`
|
||||
r := AuthenticatedRequestWithBody(app, "POST", "/api/v1/users/uqxetse3cy5eo9z2/passcode", body, sessId)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
t.Run("AliceSuccess", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
|
|
|
|||
|
|
@ -79,7 +79,14 @@ func UpdateUserPassword(router *gin.RouterGroup) {
|
|||
f := form.ChangePassword{}
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxAuthRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&f); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrInvalidPassword)
|
||||
return
|
||||
}
|
||||
|
||||
Error(c, http.StatusBadRequest, err, i18n.ErrInvalidPassword)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -49,6 +50,18 @@ func TestChangePassword(t *testing.T) {
|
|||
"{OldPassword: old}", sessId)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
UpdateUserPassword(router)
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
body := `{"OldPassword":"Alice123!","NewPassword":"` + strings.Repeat("a", int(MaxAuthRequestBytes)) + `"}`
|
||||
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/uqxetse3cy5eo9z2/password", body, sessId)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
t.Run("AliceProvidesWrongPassword", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,14 @@ func UpdateUser(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxMutationRequestBytes)
|
||||
|
||||
if err = c.BindJSON(&f); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -137,4 +138,16 @@ func TestUpdateUser(t *testing.T) {
|
|||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
}
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
UpdateUser(router)
|
||||
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
body := `{"DisplayName":"` + strings.Repeat("a", int(MaxMutationRequestBytes)) + `"}`
|
||||
r := AuthenticatedRequestWithBody(app, "PUT", "/api/v1/users/uqxetse3cy5eo9z2", body, sessId)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,15 +78,15 @@ func UploadUserFiles(router *gin.RouterGroup) {
|
|||
token := clean.Token(c.Param("token"))
|
||||
|
||||
if totalSizeLimit := conf.UploadLimitBytes(); totalSizeLimit > 0 {
|
||||
limitRequestBodyBytes(c, totalSizeLimit+maxMultipartOverheadBytes)
|
||||
LimitRequestBodyBytes(c, totalSizeLimit+MaxMultipartOverheadBytes)
|
||||
}
|
||||
|
||||
f, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
log.Errorf("upload: %s", err)
|
||||
abortRequestTooLarge(c, i18n.ErrFileTooLarge)
|
||||
AbortRequestTooLarge(c, i18n.ErrFileTooLarge)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -315,11 +315,11 @@ func ProcessUserUpload(router *gin.RouterGroup) {
|
|||
var frm form.UploadOptions
|
||||
|
||||
// Assign and validate request form values.
|
||||
limitRequestBodyBytes(c, maxUploadOptionsRequestBytes)
|
||||
LimitRequestBodyBytes(c, MaxUploadOptionsRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if isRequestBodyTooLarge(err) {
|
||||
abortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,14 @@ func PostVisionCaption(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxVisionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
c.JSON(http.StatusRequestEntityTooLarge, vision.NewApiError(request.GetId(), http.StatusRequestEntityTooLarge))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, vision.NewApiError(request.GetId(), http.StatusBadRequest))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
21
internal/api/vision_caption_test.go
Normal file
21
internal/api/vision_caption_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPostVisionCaption(t *testing.T) {
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PostVisionCaption(router)
|
||||
|
||||
body := `{"images":["data:image/jpeg;base64,` + strings.Repeat("a", int(MaxVisionRequestBytes)) + `"]}`
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/vision/caption", body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
@ -42,7 +42,14 @@ func PostVisionFace(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxVisionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
c.JSON(http.StatusRequestEntityTooLarge, vision.NewApiError(request.GetId(), http.StatusRequestEntityTooLarge))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, vision.NewApiError(request.GetId(), http.StatusBadRequest))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -178,4 +179,13 @@ func TestPostVisionFace(t *testing.T) {
|
|||
r := PerformRequest(app, http.MethodPost, "/api/v1/vision/face")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PostVisionFace(router)
|
||||
|
||||
body := `{"images":["data:image/jpeg;base64,` + strings.Repeat("a", int(MaxVisionRequestBytes)) + `"]}`
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/vision/face", body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,14 @@ func PostVisionLabels(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxVisionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
c.JSON(http.StatusRequestEntityTooLarge, vision.NewApiError(request.GetId(), http.StatusRequestEntityTooLarge))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, vision.NewApiError(request.GetId(), http.StatusBadRequest))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -129,4 +130,13 @@ func TestPostVisionLabels(t *testing.T) {
|
|||
r := PerformRequest(app, http.MethodPost, "/api/v1/vision/labels")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PostVisionLabels(router)
|
||||
|
||||
body := `{"images":["data:image/jpeg;base64,` + strings.Repeat("a", int(MaxVisionRequestBytes)) + `"]}`
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/vision/labels", body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,14 @@ func PostVisionNsfw(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxVisionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
c.JSON(http.StatusRequestEntityTooLarge, vision.NewApiError(request.GetId(), http.StatusRequestEntityTooLarge))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, vision.NewApiError(request.GetId(), http.StatusBadRequest))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -144,4 +145,13 @@ func TestPostVisionNsfw(t *testing.T) {
|
|||
r := PerformRequest(app, http.MethodPost, "/api/v1/vision/nsfw")
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
t.Run("RequestTooLarge", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
PostVisionNsfw(router)
|
||||
|
||||
body := `{"images":["data:image/jpeg;base64,` + strings.Repeat("a", int(MaxVisionRequestBytes)) + `"]}`
|
||||
r := PerformRequestWithBody(app, http.MethodPost, "/api/v1/vision/nsfw", body)
|
||||
|
||||
assert.Equal(t, http.StatusRequestEntityTooLarge, r.Code)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,14 @@ func ZipCreate(router *gin.RouterGroup) {
|
|||
start := time.Now()
|
||||
|
||||
// Assign and validate request form values.
|
||||
LimitRequestBodyBytes(c, MaxSelectionRequestBytes)
|
||||
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
if IsRequestBodyTooLarge(err) {
|
||||
AbortRequestTooLarge(c, i18n.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue