Files
OwnCord/Server/api/constants.go
T

265 lines
11 KiB
Go
Raw Normal View History

package api
import (
"math"
"sync/atomic"
"time"
"github.com/owncord/server/config"
)
// ─── Rate limits ────────────────────────────────────────────────────────────
//
// Each constant defines either a request cap or a sliding-window duration used
// by the per-endpoint rate limiters.
// authRateScaleBits holds the security.auth_rate_limit_multiplier as float
// bits. It scales the per-IP auth request caps and failure thresholds for
// deployments where many users share one IP (office/school NAT) — the
// compiled-in constants below assume roughly one person per address. Atomic
// because tests construct multiple routers concurrently. Set via
// setAuthRateScale in NewRouter; reads happen at mount time and on the login
// failure-count path.
var authRateScaleBits atomic.Uint64
func init() { authRateScaleBits.Store(math.Float64bits(1.0)) }
// setAuthRateScale clamps and installs the auth rate multiplier. Zero or
// negative (unset config) means 1.0.
func setAuthRateScale(m float64) {
if m <= 0 {
m = 1.0
}
m = math.Min(math.Max(m, 0.1), 100)
authRateScaleBits.Store(math.Float64bits(m))
}
// scaledAuthLimit applies the auth rate multiplier to a compiled-in limit,
// never returning less than 1.
func scaledAuthLimit(n int) int {
scaled := int(math.Round(float64(n) * math.Float64frombits(authRateScaleBits.Load())))
if scaled < 1 {
return 1
}
return scaled
}
const (
// registerRateLimitPerMinute is the maximum registration attempts per IP per minute.
registerRateLimitPerMinute = 3
// loginRateLimitPerMinute is the maximum login attempts per IP per minute.
loginRateLimitPerMinute = 5
// verifyTOTPRateLimitPerMinute is the maximum TOTP verification attempts per IP per minute.
verifyTOTPRateLimitPerMinute = 10
// sensitiveEndpointRateLimitPerMinute is the rate limit applied to destructive
// or sensitive endpoints (account deletion, TOTP enable/confirm/disable).
sensitiveEndpointRateLimitPerMinute = 5
// searchRateLimitPerMinute is the maximum full-text search requests per IP per minute.
searchRateLimitPerMinute = 30
// livekitProxyRateLimitPerMinute is the maximum LiveKit proxy requests per IP per minute.
livekitProxyRateLimitPerMinute = 30
// clientUpdateRateLimitPerMinute is the maximum client-update checks per IP per minute.
clientUpdateRateLimitPerMinute = 30
// gifRateLimitPerMinute is the maximum GIF proxy requests per IP per minute.
// The picker debounces at 300ms, so a user typing continuously for a minute
// stays under this; it exists to bound abuse of the operator's Klipy quota.
gifRateLimitPerMinute = 30
// loginFailureThreshold is the number of failed login attempts (within
// loginFailureWindow) before the IP is locked out.
loginFailureThreshold = 9
// loginFailureWindow is the sliding window for counting login failures.
loginFailureWindow = 15 * time.Minute
// loginLockoutDuration is how long an IP is locked out after exceeding
// loginFailureThreshold.
loginLockoutDuration = 15 * time.Minute
// deleteAccountFailureThreshold is the number of wrong-password attempts
// before the per-user lockout kicks in.
deleteAccountFailureThreshold = 3
// deleteAccountFailureWindow is the sliding window for counting
// delete-account password failures.
deleteAccountFailureWindow = 15 * time.Minute
// deleteAccountLockoutDuration is how long the account-deletion endpoint
// is locked after exceeding deleteAccountFailureThreshold.
deleteAccountLockoutDuration = 15 * time.Minute
// totpFailureRateLimit is the maximum TOTP verification failures per user
// within totpFailureWindow before the user is rate-limited.
totpFailureRateLimit = 10
// totpFailureWindow is the sliding window for counting per-user TOTP failures.
totpFailureWindow = 15 * time.Minute
// partialAuthMaxFailures is the number of failed TOTP attempts on a single
// partial-auth challenge before it is revoked.
partialAuthMaxFailures = 5
// profilePasswordRateLimitPerMinute is the maximum password change attempts
// per IP per minute.
profilePasswordRateLimitPerMinute = 5
// profileUpdateRateLimitPerMinute is the maximum profile update attempts
// per user per minute.
profileUpdateRateLimitPerMinute = 10
// loginUserFailureThreshold is the number of failed login attempts for a
// specific username (regardless of source IP) before the account is locked.
loginUserFailureThreshold = 9
// loginUserFailureWindow is the sliding window for per-username login failures.
loginUserFailureWindow = 15 * time.Minute
// loginUserLockoutDuration is how long a username is locked after exceeding
// loginUserFailureThreshold.
loginUserLockoutDuration = 15 * time.Minute
// pwConfirmFailureThreshold is the number of wrong-password attempts on
// password-confirmation endpoints before per-user lockout kicks in.
pwConfirmFailureThreshold = 3
// pwConfirmFailureWindow is the sliding window for per-user password
// confirmation failures.
pwConfirmFailureWindow = 15 * time.Minute
// pwConfirmLockoutDuration is how long password-confirmation endpoints are
// locked after exceeding pwConfirmFailureThreshold.
pwConfirmLockoutDuration = 15 * time.Minute
// uploadRateLimitPerMinute is the maximum file uploads per user per minute.
uploadRateLimitPerMinute = 10
2026-08-01 22:06:14 +02:00
// emojiUploadRateLimitPerMinute is the maximum custom-emoji uploads per
// MANAGE_SERVER holder per minute. Lower than the attachment limit: every
// accepted upload fans an emoji_update out to every connected session.
emojiUploadRateLimitPerMinute = 10
)
// ─── Timeouts & TTLs ────────────────────────────────────────────────────────
const (
// partialAuthStoreTTL is the lifetime of a partial-auth (2FA) challenge token.
partialAuthStoreTTL = 10 * time.Minute
// pendingTOTPStoreTTL is the lifetime of a pending TOTP enrollment secret.
pendingTOTPStoreTTL = 10 * time.Minute
// rateLimiterCleanupInterval is how often stale rate-limiter entries are reaped.
rateLimiterCleanupInterval = 5 * time.Minute
// rateLimiterCleanupMaxWindow is the maximum window considered when pruning
// stale rate-limiter entries. It must cover the LARGEST window any caller
// passes to Allow: slow mode (service/message_crud.go) uses windows up to
// admin's maxSlowModeSeconds (21600 s = 6 h), and a shorter horizon makes
// the reaper silently reset long slow modes after ~15 minutes.
rateLimiterCleanupMaxWindow = 6 * time.Hour
// hstsMaxAgeSeconds is the max-age value for the Strict-Transport-Security header.
hstsMaxAgeSeconds = 31536000
)
// bodyCapExemptPrefixes are the route prefixes excluded from the global 1 MiB
// body cap because they enforce their own, larger envelope at the route or
// handler level. A route with a documented cap above 1 MiB that is missing
// here is unreachable at its own limit: MaxBytesReader wrappers merely
// delegate reads, so the innermost (global) limit errors first.
var bodyCapExemptPrefixes = []string{
"/api/v1/uploads",
// 16 MiB plugin envelope enforced by the handler's own MaxBytesReader.
"/api/v1/admin/plugins/install",
// 2 MiB avatar envelope: route-scoped MaxBodySize(avatarMaxBodySize)
// plus the handler's re-wrap enforce it.
"/api/v1/users/me/avatar",
}
// ─── Size limits ────────────────────────────────────────────────────────────
const (
// defaultMaxBodySize is the default request body size limit (1 MiB).
defaultMaxBodySize = config.MaxMessageBytes
// uploadMaxBodySize is the request body size limit for file uploads (100 MiB).
uploadMaxBodySize = 100 << 20
// multipartMemoryLimit is the in-memory limit for multipart form parsing;
// data beyond this is spilled to disk.
multipartMemoryLimit = 10 << 20
// maxUploadFilenameLength is the maximum length of an upload filename
// (filesystem-safe limit).
maxUploadFilenameLength = 255
// maxAvatarURLLen is the maximum length of a user avatar URL.
maxAvatarURLLen = 512
2026-08-01 22:06:14 +02:00
// maxEmojiFileBytes is the largest custom-emoji image accepted (512 KiB).
// An emoji renders at 22px inline and 48px jumbo, so anything approaching
// this is already far more data than the pixels can use.
maxEmojiFileBytes = 512 << 10
// maxEmojiDimension caps an emoji's width and height in pixels. Discord
// normalizes to 128px; matching it means an emoji uploaded for OwnCord
// looks the same as the one it was copied from, jumbo included.
maxEmojiDimension = 128
// emojiMaxBodySize bounds the whole multipart request. The image cap plus
// the form's own framing (boundaries, headers, the shortcode field) —
// generous enough that a legitimate 512 KiB upload never trips it.
emojiMaxBodySize = 1 << 20
// emojiMultipartMemoryLimit is the in-memory limit for parsing an emoji
// upload. Above maxEmojiFileBytes, so a valid emoji never spills to disk.
emojiMultipartMemoryLimit = 1 << 20
// maxAvatarFileBytes is the largest avatar image accepted (1 MiB). An
// avatar renders at 40px in a message row and 64px in the profile popup,
// so this is already generous; the client downscales before uploading and
// the cap is what stops it being used as free file hosting.
maxAvatarFileBytes = 1 << 20
// maxAvatarDimension caps an avatar's stored width and height. Bigger than
// any surface renders it, so a retina display still has pixels to spare
// while a 6000px camera JPEG is refused rather than shipped to every
// client that sees the user post.
maxAvatarDimension = 1024
// avatarMaxBodySize bounds the whole multipart avatar request: the image
// cap plus room for the form's boundaries and headers.
avatarMaxBodySize = 2 << 20
// avatarMultipartMemoryLimit is the in-memory limit for parsing an avatar
// upload. Above maxAvatarFileBytes, so a valid avatar never spills to disk.
avatarMultipartMemoryLimit = 2 << 20
// avatarUploadRateLimitPerMinute is the maximum avatar uploads per user per
// minute. Lower than the attachment limit: every accepted upload fans a
// user_update out to every connected session and orphans the previous file.
avatarUploadRateLimitPerMinute = 5
// maxRequestIDLen bounds a client-supplied X-Request-Id. chi's
// middleware.RequestID adopts that header verbatim, and the value then
// reaches every log record for the request (logctx, requestLogger,
// recoverer) and the echoed response header — while the admin ring buffer
// retains 2000 records, so an unbounded id becomes long-lived heap.
// 128 bytes fits every common correlation-id format (UUID, 32-hex,
// W3C traceparent, chi's own "host/prefix-000001").
maxRequestIDLen = 128
// maxLoggedPathLen bounds the request path attached to a log record. The
// URL is client-controlled and net/http accepts one up to MaxHeaderBytes
// (~1 MiB), so an unbounded path fills the ring buffer the same way an
// unbounded request id does. Well past the longest real route.
maxLoggedPathLen = 256
)