2026-04-30 11:05:56 +03:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
2026-05-04 00:00:36 +03:00
|
|
|
"strconv"
|
2026-04-30 11:05:56 +03:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
|
|
|
|
"warpbox/lib/boxstore"
|
|
|
|
|
"warpbox/lib/models"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (app *App) requireAPI(ctx *gin.Context) bool {
|
|
|
|
|
if app.config.APIEnabled {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
ctx.JSON(http.StatusForbidden, gin.H{"error": "API access is disabled"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) requireGuestUploads(ctx *gin.Context) bool {
|
|
|
|
|
if app.config.GuestUploadsEnabled {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
ctx.JSON(http.StatusForbidden, gin.H{"error": "Guest uploads are disabled"})
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateCreateBoxRequest(request *models.CreateBoxRequest) error {
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateCreateBoxRequestForActor(request, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateCreateBoxRequestForActor(request *models.CreateBoxRequest, actor *requestActor) error {
|
2026-04-30 11:05:56 +03:00
|
|
|
if request == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if !app.retentionAllowed(request.RetentionKey) {
|
|
|
|
|
return fmt.Errorf("Retention option is not allowed")
|
|
|
|
|
}
|
|
|
|
|
if !app.config.ZipDownloadsEnabled {
|
|
|
|
|
allowZip := false
|
|
|
|
|
request.AllowZip = &allowZip
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(request.RetentionKey) == boxstore.OneTimeDownloadRetentionKey && !app.config.OneTimeDownloadsEnabled {
|
|
|
|
|
return fmt.Errorf("One-time downloads are disabled")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalSize := int64(0)
|
|
|
|
|
for _, file := range request.Files {
|
2026-05-04 02:27:36 +03:00
|
|
|
if err := app.validateFileSizeForActor(file.Size, actor); err != nil {
|
2026-04-30 11:05:56 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
totalSize += file.Size
|
|
|
|
|
}
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateBoxSizeForActor(totalSize, actor)
|
2026-04-30 11:05:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateIncomingFile(boxID string, size int64) error {
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateIncomingFileForActor(boxID, size, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateIncomingFileForActor(boxID string, size int64, actor *requestActor) error {
|
|
|
|
|
if err := app.validateFileSizeForActor(size, actor); err != nil {
|
2026-04-30 11:05:56 +03:00
|
|
|
return err
|
|
|
|
|
}
|
2026-05-04 02:27:36 +03:00
|
|
|
if app.effectiveMaxBoxBytes(actor) <= 0 {
|
2026-04-30 11:05:56 +03:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
files, err := boxstore.ListFiles(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
totalSize := size
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
totalSize += file.Size
|
|
|
|
|
}
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateBoxSizeForActor(totalSize, actor)
|
2026-04-30 11:05:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateManifestFileUpload(boxID string, fileID string, size int64) error {
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateManifestFileUploadForActor(boxID, fileID, size, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateManifestFileUploadForActor(boxID string, fileID string, size int64, actor *requestActor) error {
|
|
|
|
|
if err := app.validateFileSizeForActor(size, actor); err != nil {
|
2026-04-30 11:05:56 +03:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manifest, err := boxstore.ReadManifest(boxID)
|
|
|
|
|
if err != nil {
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateIncomingFileForActor(boxID, size, actor)
|
2026-04-30 11:05:56 +03:00
|
|
|
}
|
|
|
|
|
if boxstore.IsExpired(manifest) {
|
|
|
|
|
_ = boxstore.DeleteBox(boxID)
|
|
|
|
|
return fmt.Errorf("Box expired")
|
|
|
|
|
}
|
2026-05-04 02:27:36 +03:00
|
|
|
if app.effectiveMaxBoxBytes(actor) <= 0 {
|
2026-04-30 11:05:56 +03:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
totalSize := int64(0)
|
|
|
|
|
found := false
|
|
|
|
|
for _, file := range manifest.Files {
|
|
|
|
|
if file.ID == fileID {
|
|
|
|
|
totalSize += size
|
|
|
|
|
found = true
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
totalSize += file.Size
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
totalSize += size
|
|
|
|
|
}
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateBoxSizeForActor(totalSize, actor)
|
2026-04-30 11:05:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateFileSize(size int64) error {
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateFileSizeForActor(size, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) effectiveMaxFileBytes(actor *requestActor) int64 {
|
|
|
|
|
if actor == nil {
|
|
|
|
|
return app.config.GlobalMaxFileSizeBytes
|
|
|
|
|
}
|
|
|
|
|
return actor.User.Limits.MaxFileSizeBytes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) effectiveMaxBoxBytes(actor *requestActor) int64 {
|
|
|
|
|
if actor == nil {
|
|
|
|
|
return app.config.GlobalMaxBoxSizeBytes
|
|
|
|
|
}
|
|
|
|
|
return actor.User.Limits.MaxBoxSizeBytes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateFileSizeForActor(size int64, actor *requestActor) error {
|
2026-04-30 11:05:56 +03:00
|
|
|
if size < 0 {
|
|
|
|
|
return fmt.Errorf("File size cannot be negative")
|
|
|
|
|
}
|
2026-05-04 02:27:36 +03:00
|
|
|
limit := app.effectiveMaxFileBytes(actor)
|
|
|
|
|
if limit > 0 && size > limit {
|
|
|
|
|
if actor != nil {
|
|
|
|
|
return fmt.Errorf("File exceeds this account's max file size")
|
|
|
|
|
}
|
2026-04-30 11:05:56 +03:00
|
|
|
return fmt.Errorf("File exceeds the global max file size")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateBoxSize(size int64) error {
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.validateBoxSizeForActor(size, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) validateBoxSizeForActor(size int64, actor *requestActor) error {
|
2026-04-30 11:05:56 +03:00
|
|
|
if size < 0 {
|
|
|
|
|
return fmt.Errorf("Box size cannot be negative")
|
|
|
|
|
}
|
2026-05-04 02:27:36 +03:00
|
|
|
limit := app.effectiveMaxBoxBytes(actor)
|
|
|
|
|
if limit > 0 && size > limit {
|
|
|
|
|
if actor != nil {
|
|
|
|
|
return fmt.Errorf("Box exceeds this account's max box size")
|
|
|
|
|
}
|
2026-04-30 11:05:56 +03:00
|
|
|
return fmt.Errorf("Box exceeds the global max box size")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) rejectExpiredManifestBox(boxID string) error {
|
|
|
|
|
manifest, err := boxstore.ReadManifest(boxID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if !boxstore.IsExpired(manifest) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
_ = boxstore.DeleteBox(boxID)
|
|
|
|
|
return fmt.Errorf("Box expired")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) limitRequestBody(ctx *gin.Context) {
|
2026-05-04 02:27:36 +03:00
|
|
|
app.limitRequestBodyForActor(ctx, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) limitRequestBodyForActor(ctx *gin.Context, actor *requestActor) {
|
|
|
|
|
limit := app.maxRequestBodyBytesForActor(actor)
|
2026-04-30 11:05:56 +03:00
|
|
|
if limit <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, limit)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) maxRequestBodyBytes() int64 {
|
2026-05-04 02:27:36 +03:00
|
|
|
return app.maxRequestBodyBytesForActor(nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (app *App) maxRequestBodyBytesForActor(actor *requestActor) int64 {
|
|
|
|
|
limit := app.effectiveMaxBoxBytes(actor)
|
|
|
|
|
fileLimit := app.effectiveMaxFileBytes(actor)
|
|
|
|
|
if limit <= 0 || fileLimit > limit {
|
|
|
|
|
limit = fileLimit
|
2026-04-30 11:05:56 +03:00
|
|
|
}
|
|
|
|
|
if limit <= 0 {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return limit + 10*1024*1024
|
|
|
|
|
}
|
2026-05-04 00:00:36 +03:00
|
|
|
|
|
|
|
|
func (app *App) enforceUploadRateLimit(ctx *gin.Context, size int64) bool {
|
|
|
|
|
if !app.securityFeaturesEnabled() || app.securityGuard == nil {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
ip := app.clientIP(ctx)
|
|
|
|
|
if app.securityGuard.IsWhitelisted(ip) || app.securityGuard.IsAdminWhitelisted(ip) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
allowed, requestCount, totalBytes := app.securityGuard.AllowUpload(
|
|
|
|
|
ip,
|
|
|
|
|
size,
|
|
|
|
|
app.config.SecurityUploadWindowSeconds,
|
|
|
|
|
app.config.SecurityUploadMaxRequests,
|
|
|
|
|
app.config.SecurityUploadMaxBytes,
|
|
|
|
|
)
|
|
|
|
|
if allowed {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
app.logActivity("security.upload_limit", "high", "Upload rate limit exceeded", ctx, map[string]string{
|
|
|
|
|
"requests": strconv.Itoa(requestCount),
|
|
|
|
|
"bytes": strconv.FormatInt(totalBytes, 10),
|
|
|
|
|
})
|
|
|
|
|
app.createAlert(
|
|
|
|
|
"Upload rate limit triggered",
|
|
|
|
|
"medium",
|
|
|
|
|
"security",
|
|
|
|
|
"430",
|
|
|
|
|
"security.upload.rate_limit",
|
|
|
|
|
"Per-IP upload rate limit blocked request.",
|
|
|
|
|
map[string]string{"ip": ip, "requests": strconv.Itoa(requestCount)},
|
|
|
|
|
)
|
|
|
|
|
ctx.JSON(http.StatusTooManyRequests, gin.H{"error": "Too many uploads from this IP. Try again later."})
|
|
|
|
|
return false
|
|
|
|
|
}
|