mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-11-10 14:44:04 +08:00
Send a Retry-After header with rate limit 429 responses
This commit is contained in:
1 parent
b916f216fc
commit
5306dfaa97
9 files changed
+376
-216
No files matched your search
+134
-113
@@ -3,18 +3,22 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/v2/log"
|
||||
)
|
||||
|
||||
// errHTTP is a generic HTTP error for any non-200 HTTP error
|
||||
type errHTTP struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
HTTPCode int `json:"http"`
|
||||
Message string `json:"error"`
|
||||
Link string `json:"link,omitempty"`
|
||||
context log.Context
|
||||
Code int `json:"code,omitempty"`
|
||||
HTTPCode int `json:"http"`
|
||||
Message string `json:"error"`
|
||||
Link string `json:"link,omitempty"`
|
||||
context log.Context
|
||||
retryAfter time.Duration // How long until a 429 would be admitted, sent as Retry-After; 0 if unknown
|
||||
}
|
||||
|
||||
func (e errHTTP) Error() string {
|
||||
@@ -64,20 +68,37 @@ func (e errHTTP) Fields(context log.Context) *errHTTP {
|
||||
return &c
|
||||
}
|
||||
|
||||
// RetryAfter returns a copy of the error that tells the client to retry after d (see limit.Decision)
|
||||
func (e errHTTP) RetryAfter(d time.Duration) *errHTTP {
|
||||
c := e.clone()
|
||||
c.retryAfter = d
|
||||
return &c
|
||||
}
|
||||
|
||||
func (e errHTTP) clone() errHTTP {
|
||||
context := make(log.Context)
|
||||
for k, v := range e.context {
|
||||
context[k] = v
|
||||
}
|
||||
return errHTTP{
|
||||
Code: e.Code,
|
||||
HTTPCode: e.HTTPCode,
|
||||
Message: e.Message,
|
||||
Link: e.Link,
|
||||
context: context,
|
||||
Code: e.Code,
|
||||
HTTPCode: e.HTTPCode,
|
||||
Message: e.Message,
|
||||
Link: e.Link,
|
||||
context: context,
|
||||
retryAfter: e.retryAfter,
|
||||
}
|
||||
}
|
||||
|
||||
// retryAfterSeconds is the Retry-After header value: whole seconds rounded up, so a client never
|
||||
// retries too early, or "" if the wait is unknown
|
||||
func (e errHTTP) retryAfterSeconds() string {
|
||||
if e.retryAfter <= 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(int64(math.Ceil(e.retryAfter.Seconds())), 10)
|
||||
}
|
||||
|
||||
// errWebSocketPostUpgrade is a wrapper error indicating an error occurred after the WebSocket
|
||||
// upgrade completed (i.e., the connection was hijacked). This is used to avoid calling
|
||||
// WriteHeader on hijacked connections, which causes log spam.
|
||||
@@ -94,107 +115,107 @@ func (e *errWebSocketPostUpgrade) Unwrap() error {
|
||||
}
|
||||
|
||||
var (
|
||||
errHTTPBadRequest = &errHTTP{40000, http.StatusBadRequest, "invalid request", "", nil}
|
||||
errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications", nil}
|
||||
errHTTPBadRequestDelayNoCache = &errHTTP{40002, http.StatusBadRequest, "cannot disable cache for delayed message", "", nil}
|
||||
errHTTPBadRequestDelayNoEmail = &errHTTP{40003, http.StatusBadRequest, "delayed e-mail notifications are not supported", "", nil}
|
||||
errHTTPBadRequestDelayCannotParse = &errHTTP{40004, http.StatusBadRequest, "invalid delay parameter: unable to parse delay", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil}
|
||||
errHTTPBadRequestDelayTooSmall = &errHTTP{40005, http.StatusBadRequest, "invalid delay parameter: too small, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil}
|
||||
errHTTPBadRequestDelayTooLarge = &errHTTP{40006, http.StatusBadRequest, "invalid delay parameter: too large, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil}
|
||||
errHTTPBadRequestPriorityInvalid = &errHTTP{40007, http.StatusBadRequest, "invalid priority parameter", "https://ntfy.sh/docs/publish/#message-priority", nil}
|
||||
errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages", nil}
|
||||
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid request: topic invalid", "", nil}
|
||||
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid request: topic name is not allowed", "", nil}
|
||||
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid request: message must be UTF-8 encoded", "", nil}
|
||||
errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", "https://ntfy.sh/docs/publish/#attachments", nil}
|
||||
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachments not allowed", "https://ntfy.sh/docs/config/#attachments", nil}
|
||||
errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil}
|
||||
errHTTPBadRequestWebSocketsUpgradeHeaderMissing = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", "https://ntfy.sh/docs/subscribe/api/#websockets", nil}
|
||||
errHTTPBadRequestMessageJSONInvalid = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json", nil}
|
||||
errHTTPBadRequestActionsInvalid = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions invalid", "https://ntfy.sh/docs/publish/#action-buttons", nil}
|
||||
errHTTPBadRequestMatrixMessageInvalid = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway", nil}
|
||||
errHTTPBadRequestIconURLInvalid = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons", nil}
|
||||
errHTTPBadRequestSignupNotEnabled = &errHTTP{40022, http.StatusBadRequest, "invalid request: signup not enabled", "https://ntfy.sh/docs/config", nil}
|
||||
errHTTPBadRequestNoTokenProvided = &errHTTP{40023, http.StatusBadRequest, "invalid request: no token provided", "", nil}
|
||||
errHTTPBadRequestJSONInvalid = &errHTTP{40024, http.StatusBadRequest, "invalid request: request body must be valid JSON", "", nil}
|
||||
errHTTPBadRequestPermissionInvalid = &errHTTP{40025, http.StatusBadRequest, "invalid request: incorrect permission string", "", nil}
|
||||
errHTTPBadRequestIncorrectPasswordConfirmation = &errHTTP{40026, http.StatusBadRequest, "invalid request: password confirmation is not correct", "", nil}
|
||||
errHTTPBadRequestNotAPaidUser = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", "", nil}
|
||||
errHTTPBadRequestBillingRequestInvalid = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid billing request", "", nil}
|
||||
errHTTPBadRequestBillingSubscriptionExists = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil}
|
||||
errHTTPBadRequestTierInvalid = &errHTTP{40030, http.StatusBadRequest, "invalid request: tier does not exist", "", nil}
|
||||
errHTTPBadRequestUserNotFound = &errHTTP{40031, http.StatusBadRequest, "invalid request: user does not exist", "", nil}
|
||||
errHTTPBadRequestPhoneCallsDisabled = &errHTTP{40032, http.StatusBadRequest, "invalid request: calling is disabled", "https://ntfy.sh/docs/config/#phone-calls", nil}
|
||||
errHTTPBadRequestPhoneNumberInvalid = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil}
|
||||
errHTTPBadRequestPhoneNumberNotVerified = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil}
|
||||
errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil}
|
||||
errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil}
|
||||
errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "invalid request: delayed call notifications are not supported", "", nil}
|
||||
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil}
|
||||
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil}
|
||||
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
|
||||
errHTTPBadRequestTemplateMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateInvalid = &errHTTP{40043, http.StatusBadRequest, "invalid request: could not parse template", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, define, or block", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateExecutionTimeout = &errHTTP{40055, http.StatusBadRequest, "invalid request: template execution timed out", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
|
||||
errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#updating-deleting-notifications", nil}
|
||||
errHTTPBadRequestEmailAddressInvalid = &errHTTP{40050, http.StatusBadRequest, "invalid request: invalid e-mail address", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil}
|
||||
errHTTPBadRequestEmailVerificationLinkInvalid = &errHTTP{40051, http.StatusBadRequest, "invalid request: email verification link invalid or expired", "", nil}
|
||||
errHTTPBadRequestEmailAddressNotVerified = &errHTTP{40052, http.StatusBadRequest, "invalid request: email address not verified", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil}
|
||||
errHTTPBadRequestAnonymousEmailNotAllowed = &errHTTP{40053, http.StatusBadRequest, "invalid request: anonymous email sending is not allowed", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil}
|
||||
errHTTPBadRequestResetLinkInvalid = &errHTTP{40054, http.StatusBadRequest, "invalid request: password reset link invalid or expired", "", nil}
|
||||
errHTTPBadRequestTemplateTooLarge = &errHTTP{40056, http.StatusBadRequest, "invalid request: template too large", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||
errHTTPBadRequestTitleTooLarge = &errHTTP{40057, http.StatusBadRequest, "invalid request: title is too large", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPBadRequestTagsTooLarge = &errHTTP{40058, http.StatusBadRequest, "invalid request: tags are too large", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPBadRequestTimezoneInvalid = &errHTTP{40059, http.StatusBadRequest, "invalid request: invalid timezone", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil}
|
||||
errHTTPBadRequestPhoneVerificationCodeInvalid = &errHTTP{40060, http.StatusBadRequest, "invalid request: phone number verification code invalid", "", nil}
|
||||
errHTTPBadRequestTooManyTopics = &errHTTP{40061, http.StatusBadRequest, "invalid request: too many topics", "https://ntfy.sh/docs/subscribe/api/#subscribe-to-multiple-topics", nil}
|
||||
errHTTPBadRequestTierCodeInvalid = &errHTTP{40062, http.StatusBadRequest, "invalid request: tier code invalid, or tier name missing", "", nil}
|
||||
errHTTPBadRequestTierReservationsLimit = &errHTTP{40063, http.StatusBadRequest, "invalid request: user has more topic reservations than the tier allows, remove reservations first", "", nil}
|
||||
errHTTPBadRequestAccessAdminUser = &errHTTP{40064, http.StatusBadRequest, "invalid request: admins can access all topics, access control entries are not allowed", "", nil}
|
||||
errHTTPBadRequestTokenNotFound = &errHTTP{40065, http.StatusBadRequest, "invalid request: token does not exist", "", nil}
|
||||
errHTTPBadRequestRoleInvalid = &errHTTP{40066, http.StatusBadRequest, "invalid request: role must be user or admin", "", nil}
|
||||
errHTTPBadRequestUserNoPrimaryEmail = &errHTTP{40067, http.StatusBadRequest, "invalid request: user has no primary email, cannot send reset link", "", nil}
|
||||
errHTTPBadRequestBaseURLNotConfigured = &errHTTP{40068, http.StatusBadRequest, "invalid request: base-url must be configured on the server to generate links", "https://ntfy.sh/docs/config/", nil}
|
||||
errHTTPBadRequestPasswordHashInvalid = &errHTTP{40069, http.StatusBadRequest, "invalid request: password hash must be a bcrypt hash with cost 10 or higher, use 'ntfy user hash' to generate", "", nil}
|
||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||
errHTTPForbiddenAdminSocketOnly = &errHTTP{40302, http.StatusForbidden, "forbidden: this operation is only allowed over the local admin socket", "https://ntfy.sh/docs/config/#admin-socket", nil}
|
||||
errHTTPConflictUserExists = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", "", nil}
|
||||
errHTTPConflictTopicReserved = &errHTTP{40902, http.StatusConflict, "conflict: access control entry for topic or topic pattern already exists", "", nil}
|
||||
errHTTPConflictSubscriptionExists = &errHTTP{40903, http.StatusConflict, "conflict: topic subscription already exists", "", nil}
|
||||
errHTTPConflictPhoneNumberExists = &errHTTP{40904, http.StatusConflict, "conflict: phone number already exists", "", nil}
|
||||
errHTTPConflictProvisionedUserChange = &errHTTP{40905, http.StatusConflict, "conflict: cannot change or delete provisioned user", "", nil}
|
||||
errHTTPConflictProvisionedTokenChange = &errHTTP{40906, http.StatusConflict, "conflict: cannot change or delete provisioned token", "", nil}
|
||||
errHTTPConflictEmailExists = &errHTTP{40907, http.StatusConflict, "conflict: email address already exists", "", nil}
|
||||
errHTTPConflictEmailPrimaryElsewhere = &errHTTP{40908, http.StatusConflict, "conflict: email address is the primary email on another account", "", nil}
|
||||
errHTTPConflictTierExists = &errHTTP{40909, http.StatusConflict, "conflict: tier already exists", "", nil}
|
||||
errHTTPConflictTierInUse = &errHTTP{40910, http.StatusConflict, "conflict: tier is still assigned to users", "", nil}
|
||||
errHTTPGonePhoneVerificationExpired = &errHTTP{41001, http.StatusGone, "phone number verification expired or does not exist", "", nil}
|
||||
errHTTPEntityTooLargeAttachment = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPEntityTooLargeMatrixRequest = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", "", nil}
|
||||
errHTTPEntityTooLargeJSONBody = &errHTTP{41303, http.StatusRequestEntityTooLarge, "JSON body too large", "", nil}
|
||||
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPTooManyRequestsLimitAttachmentBandwidth = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth reached", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPTooManyRequestsLimitAccountActions = &errHTTP{42906, http.StatusTooManyRequests, "limit reached: too many account requests", "https://ntfy.sh/docs/publish/#limitations", nil} // FIXME document limit
|
||||
errHTTPTooManyRequestsLimitReservations = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", "", nil}
|
||||
errHTTPTooManyRequestsLimitMessages = &errHTTP{42908, http.StatusTooManyRequests, "limit reached: daily message quota reached", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPTooManyRequestsLimitAuthFailure = &errHTTP{42909, http.StatusTooManyRequests, "limit reached: too many auth failures", "https://ntfy.sh/docs/publish/#limitations", nil} // FIXME document limit
|
||||
errHTTPTooManyRequestsLimitCalls = &errHTTP{42910, http.StatusTooManyRequests, "limit reached: daily phone call quota reached", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPTooManyRequestsLimitTopicCreation = &errHTTP{42911, http.StatusTooManyRequests, "limit reached: too many new topics, please wait", "https://ntfy.sh/docs/publish/#limitations", nil}
|
||||
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil}
|
||||
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil}
|
||||
errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil}
|
||||
errHTTPInternalErrorWebPushUnableToPublish = &errHTTP{50004, http.StatusInternalServerError, "internal server error: unable to publish web push message", "", nil}
|
||||
errHTTPServiceUnavailableDraining = &errHTTP{50301, http.StatusServiceUnavailable, "service unavailable: this node is shutting down, please reconnect", "", nil}
|
||||
errHTTPInsufficientStorageUnifiedPush = &errHTTP{50701, http.StatusInsufficientStorage, "cannot publish to UnifiedPush topic without previously active subscriber", "", nil}
|
||||
errHTTPBadRequest = &errHTTP{40000, http.StatusBadRequest, "invalid request", "", nil, 0}
|
||||
errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications", nil, 0}
|
||||
errHTTPBadRequestDelayNoCache = &errHTTP{40002, http.StatusBadRequest, "cannot disable cache for delayed message", "", nil, 0}
|
||||
errHTTPBadRequestDelayNoEmail = &errHTTP{40003, http.StatusBadRequest, "delayed e-mail notifications are not supported", "", nil, 0}
|
||||
errHTTPBadRequestDelayCannotParse = &errHTTP{40004, http.StatusBadRequest, "invalid delay parameter: unable to parse delay", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil, 0}
|
||||
errHTTPBadRequestDelayTooSmall = &errHTTP{40005, http.StatusBadRequest, "invalid delay parameter: too small, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil, 0}
|
||||
errHTTPBadRequestDelayTooLarge = &errHTTP{40006, http.StatusBadRequest, "invalid delay parameter: too large, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil, 0}
|
||||
errHTTPBadRequestPriorityInvalid = &errHTTP{40007, http.StatusBadRequest, "invalid priority parameter", "https://ntfy.sh/docs/publish/#message-priority", nil, 0}
|
||||
errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages", nil, 0}
|
||||
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid request: topic invalid", "", nil, 0}
|
||||
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid request: topic name is not allowed", "", nil, 0}
|
||||
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid request: message must be UTF-8 encoded", "", nil, 0}
|
||||
errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", "https://ntfy.sh/docs/publish/#attachments", nil, 0}
|
||||
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachments not allowed", "https://ntfy.sh/docs/config/#attachments", nil, 0}
|
||||
errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil, 0}
|
||||
errHTTPBadRequestWebSocketsUpgradeHeaderMissing = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", "https://ntfy.sh/docs/subscribe/api/#websockets", nil, 0}
|
||||
errHTTPBadRequestMessageJSONInvalid = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json", nil, 0}
|
||||
errHTTPBadRequestActionsInvalid = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions invalid", "https://ntfy.sh/docs/publish/#action-buttons", nil, 0}
|
||||
errHTTPBadRequestMatrixMessageInvalid = &errHTTP{40019, http.StatusBadRequest, "invalid request: Matrix JSON invalid", "https://ntfy.sh/docs/publish/#matrix-gateway", nil, 0}
|
||||
errHTTPBadRequestIconURLInvalid = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons", nil, 0}
|
||||
errHTTPBadRequestSignupNotEnabled = &errHTTP{40022, http.StatusBadRequest, "invalid request: signup not enabled", "https://ntfy.sh/docs/config", nil, 0}
|
||||
errHTTPBadRequestNoTokenProvided = &errHTTP{40023, http.StatusBadRequest, "invalid request: no token provided", "", nil, 0}
|
||||
errHTTPBadRequestJSONInvalid = &errHTTP{40024, http.StatusBadRequest, "invalid request: request body must be valid JSON", "", nil, 0}
|
||||
errHTTPBadRequestPermissionInvalid = &errHTTP{40025, http.StatusBadRequest, "invalid request: incorrect permission string", "", nil, 0}
|
||||
errHTTPBadRequestIncorrectPasswordConfirmation = &errHTTP{40026, http.StatusBadRequest, "invalid request: password confirmation is not correct", "", nil, 0}
|
||||
errHTTPBadRequestNotAPaidUser = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", "", nil, 0}
|
||||
errHTTPBadRequestBillingRequestInvalid = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid billing request", "", nil, 0}
|
||||
errHTTPBadRequestBillingSubscriptionExists = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil, 0}
|
||||
errHTTPBadRequestTierInvalid = &errHTTP{40030, http.StatusBadRequest, "invalid request: tier does not exist", "", nil, 0}
|
||||
errHTTPBadRequestUserNotFound = &errHTTP{40031, http.StatusBadRequest, "invalid request: user does not exist", "", nil, 0}
|
||||
errHTTPBadRequestPhoneCallsDisabled = &errHTTP{40032, http.StatusBadRequest, "invalid request: calling is disabled", "https://ntfy.sh/docs/config/#phone-calls", nil, 0}
|
||||
errHTTPBadRequestPhoneNumberInvalid = &errHTTP{40033, http.StatusBadRequest, "invalid request: phone number invalid", "https://ntfy.sh/docs/publish/#phone-calls", nil, 0}
|
||||
errHTTPBadRequestPhoneNumberNotVerified = &errHTTP{40034, http.StatusBadRequest, "invalid request: phone number not verified, or no matching verified numbers found", "https://ntfy.sh/docs/publish/#phone-calls", nil, 0}
|
||||
errHTTPBadRequestAnonymousCallsNotAllowed = &errHTTP{40035, http.StatusBadRequest, "invalid request: anonymous phone calls are not allowed", "https://ntfy.sh/docs/publish/#phone-calls", nil, 0}
|
||||
errHTTPBadRequestPhoneNumberVerifyChannelInvalid = &errHTTP{40036, http.StatusBadRequest, "invalid request: verification channel must be 'sms' or 'call'", "https://ntfy.sh/docs/publish/#phone-calls", nil, 0}
|
||||
errHTTPBadRequestDelayNoCall = &errHTTP{40037, http.StatusBadRequest, "invalid request: delayed call notifications are not supported", "", nil, 0}
|
||||
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil, 0}
|
||||
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil, 0}
|
||||
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil, 0}
|
||||
errHTTPBadRequestTemplateMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestTemplateMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestTemplateInvalid = &errHTTP{40043, http.StatusBadRequest, "invalid request: could not parse template", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, define, or block", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestTemplateExecutionTimeout = &errHTTP{40055, http.StatusBadRequest, "invalid request: template execution timed out", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil, 0}
|
||||
errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#updating-deleting-notifications", nil, 0}
|
||||
errHTTPBadRequestEmailAddressInvalid = &errHTTP{40050, http.StatusBadRequest, "invalid request: invalid e-mail address", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil, 0}
|
||||
errHTTPBadRequestEmailVerificationLinkInvalid = &errHTTP{40051, http.StatusBadRequest, "invalid request: email verification link invalid or expired", "", nil, 0}
|
||||
errHTTPBadRequestEmailAddressNotVerified = &errHTTP{40052, http.StatusBadRequest, "invalid request: email address not verified", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil, 0}
|
||||
errHTTPBadRequestAnonymousEmailNotAllowed = &errHTTP{40053, http.StatusBadRequest, "invalid request: anonymous email sending is not allowed", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil, 0}
|
||||
errHTTPBadRequestResetLinkInvalid = &errHTTP{40054, http.StatusBadRequest, "invalid request: password reset link invalid or expired", "", nil, 0}
|
||||
errHTTPBadRequestTemplateTooLarge = &errHTTP{40056, http.StatusBadRequest, "invalid request: template too large", "https://ntfy.sh/docs/publish/#message-templating", nil, 0}
|
||||
errHTTPBadRequestTitleTooLarge = &errHTTP{40057, http.StatusBadRequest, "invalid request: title is too large", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPBadRequestTagsTooLarge = &errHTTP{40058, http.StatusBadRequest, "invalid request: tags are too large", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPBadRequestTimezoneInvalid = &errHTTP{40059, http.StatusBadRequest, "invalid request: invalid timezone", "https://ntfy.sh/docs/publish/#scheduled-delivery", nil, 0}
|
||||
errHTTPBadRequestPhoneVerificationCodeInvalid = &errHTTP{40060, http.StatusBadRequest, "invalid request: phone number verification code invalid", "", nil, 0}
|
||||
errHTTPBadRequestTooManyTopics = &errHTTP{40061, http.StatusBadRequest, "invalid request: too many topics", "https://ntfy.sh/docs/subscribe/api/#subscribe-to-multiple-topics", nil, 0}
|
||||
errHTTPBadRequestTierCodeInvalid = &errHTTP{40062, http.StatusBadRequest, "invalid request: tier code invalid, or tier name missing", "", nil, 0}
|
||||
errHTTPBadRequestTierReservationsLimit = &errHTTP{40063, http.StatusBadRequest, "invalid request: user has more topic reservations than the tier allows, remove reservations first", "", nil, 0}
|
||||
errHTTPBadRequestAccessAdminUser = &errHTTP{40064, http.StatusBadRequest, "invalid request: admins can access all topics, access control entries are not allowed", "", nil, 0}
|
||||
errHTTPBadRequestTokenNotFound = &errHTTP{40065, http.StatusBadRequest, "invalid request: token does not exist", "", nil, 0}
|
||||
errHTTPBadRequestRoleInvalid = &errHTTP{40066, http.StatusBadRequest, "invalid request: role must be user or admin", "", nil, 0}
|
||||
errHTTPBadRequestUserNoPrimaryEmail = &errHTTP{40067, http.StatusBadRequest, "invalid request: user has no primary email, cannot send reset link", "", nil, 0}
|
||||
errHTTPBadRequestBaseURLNotConfigured = &errHTTP{40068, http.StatusBadRequest, "invalid request: base-url must be configured on the server to generate links", "https://ntfy.sh/docs/config/", nil, 0}
|
||||
errHTTPBadRequestPasswordHashInvalid = &errHTTP{40069, http.StatusBadRequest, "invalid request: password hash must be a bcrypt hash with cost 10 or higher, use 'ntfy user hash' to generate", "", nil, 0}
|
||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil, 0}
|
||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil, 0}
|
||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil, 0}
|
||||
errHTTPForbiddenAdminSocketOnly = &errHTTP{40302, http.StatusForbidden, "forbidden: this operation is only allowed over the local admin socket", "https://ntfy.sh/docs/config/#admin-socket", nil, 0}
|
||||
errHTTPConflictUserExists = &errHTTP{40901, http.StatusConflict, "conflict: user already exists", "", nil, 0}
|
||||
errHTTPConflictTopicReserved = &errHTTP{40902, http.StatusConflict, "conflict: access control entry for topic or topic pattern already exists", "", nil, 0}
|
||||
errHTTPConflictSubscriptionExists = &errHTTP{40903, http.StatusConflict, "conflict: topic subscription already exists", "", nil, 0}
|
||||
errHTTPConflictPhoneNumberExists = &errHTTP{40904, http.StatusConflict, "conflict: phone number already exists", "", nil, 0}
|
||||
errHTTPConflictProvisionedUserChange = &errHTTP{40905, http.StatusConflict, "conflict: cannot change or delete provisioned user", "", nil, 0}
|
||||
errHTTPConflictProvisionedTokenChange = &errHTTP{40906, http.StatusConflict, "conflict: cannot change or delete provisioned token", "", nil, 0}
|
||||
errHTTPConflictEmailExists = &errHTTP{40907, http.StatusConflict, "conflict: email address already exists", "", nil, 0}
|
||||
errHTTPConflictEmailPrimaryElsewhere = &errHTTP{40908, http.StatusConflict, "conflict: email address is the primary email on another account", "", nil, 0}
|
||||
errHTTPConflictTierExists = &errHTTP{40909, http.StatusConflict, "conflict: tier already exists", "", nil, 0}
|
||||
errHTTPConflictTierInUse = &errHTTP{40910, http.StatusConflict, "conflict: tier is still assigned to users", "", nil, 0}
|
||||
errHTTPGonePhoneVerificationExpired = &errHTTP{41001, http.StatusGone, "phone number verification expired or does not exist", "", nil, 0}
|
||||
errHTTPEntityTooLargeAttachment = &errHTTP{41301, http.StatusRequestEntityTooLarge, "attachment too large, or bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPEntityTooLargeMatrixRequest = &errHTTP{41302, http.StatusRequestEntityTooLarge, "Matrix request is larger than the max allowed length", "", nil, 0}
|
||||
errHTTPEntityTooLargeJSONBody = &errHTTP{41303, http.StatusRequestEntityTooLarge, "JSON body too large", "", nil, 0}
|
||||
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPTooManyRequestsLimitAttachmentBandwidth = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth reached", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPTooManyRequestsLimitAccountActions = &errHTTP{42906, http.StatusTooManyRequests, "limit reached: too many account requests", "https://ntfy.sh/docs/publish/#limitations", nil, 0} // FIXME document limit
|
||||
errHTTPTooManyRequestsLimitReservations = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", "", nil, 0}
|
||||
errHTTPTooManyRequestsLimitMessages = &errHTTP{42908, http.StatusTooManyRequests, "limit reached: daily message quota reached", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPTooManyRequestsLimitAuthFailure = &errHTTP{42909, http.StatusTooManyRequests, "limit reached: too many auth failures", "https://ntfy.sh/docs/publish/#limitations", nil, 0} // FIXME document limit
|
||||
errHTTPTooManyRequestsLimitCalls = &errHTTP{42910, http.StatusTooManyRequests, "limit reached: daily phone call quota reached", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPTooManyRequestsLimitTopicCreation = &errHTTP{42911, http.StatusTooManyRequests, "limit reached: too many new topics, please wait", "https://ntfy.sh/docs/publish/#limitations", nil, 0}
|
||||
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil, 0}
|
||||
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil, 0}
|
||||
errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil, 0}
|
||||
errHTTPInternalErrorWebPushUnableToPublish = &errHTTP{50004, http.StatusInternalServerError, "internal server error: unable to publish web push message", "", nil, 0}
|
||||
errHTTPServiceUnavailableDraining = &errHTTP{50301, http.StatusServiceUnavailable, "service unavailable: this node is shutting down, please reconnect", "", nil, 0}
|
||||
errHTTPInsufficientStorageUnifiedPush = &errHTTP{50701, http.StatusInsufficientStorage, "cannot publish to UnifiedPush topic without previously active subscriber", "", nil, 0}
|
||||
)
|
||||
+25
-14
@@ -701,6 +701,9 @@ func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor,
|
||||
isRateLimiting := util.Contains(rateLimitingErrorCodes, httpErr.HTTPCode)
|
||||
isNormalError := strings.Contains(err.Error(), "i/o timeout") || util.Contains(normalErrorCodes, httpErr.HTTPCode) ||
|
||||
httpErr.Code == errHTTPServiceUnavailableDraining.Code // A draining node rejects every new subscription, by design
|
||||
if retryAfter := httpErr.retryAfterSeconds(); retryAfter != "" {
|
||||
w.Header().Set("Retry-After", retryAfter)
|
||||
}
|
||||
ev := logvr(v, r).Err(err)
|
||||
if websocket.IsWebSocketUpgrade(r) {
|
||||
ev.Tag(tagWebsocket).Fields(websocketErrorContext(err))
|
||||
@@ -1019,8 +1022,8 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
|
||||
} else if m.Sender.IsValid() {
|
||||
bandwidthVisitor = s.visitor(m.Sender, nil)
|
||||
}
|
||||
if !bandwidthVisitor.BandwidthAllowed(size) {
|
||||
return errHTTPTooManyRequestsLimitAttachmentBandwidth.With(m)
|
||||
if d := bandwidthVisitor.BandwidthAllowed(size); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitAttachmentBandwidth.With(m).RetryAfter(d.RetryAfter)
|
||||
}
|
||||
// Actually send file
|
||||
if m.Attachment.Name != "" {
|
||||
@@ -1105,16 +1108,18 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*model.Mess
|
||||
// the subscription as invalid if any 400-499 code (except 429/408) is returned.
|
||||
// See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46
|
||||
return nil, errHTTPInsufficientStorageUnifiedPush.With(t)
|
||||
} else if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() {
|
||||
return nil, errHTTPTooManyRequestsLimitMessages.With(t)
|
||||
} else if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) {
|
||||
if d := vrate.MessageAllowed(); !d.OK {
|
||||
return nil, errHTTPTooManyRequestsLimitMessages.With(t).RetryAfter(d.RetryAfter)
|
||||
}
|
||||
}
|
||||
if email != "" {
|
||||
var httpErr *errHTTP
|
||||
email, httpErr = s.convertEmailAddress(v.User(), email)
|
||||
if httpErr != nil {
|
||||
return nil, httpErr.With(t)
|
||||
} else if !vrate.EmailAllowed() {
|
||||
return nil, errHTTPTooManyRequestsLimitEmails.With(t)
|
||||
} else if d := vrate.EmailAllowed(); !d.OK {
|
||||
return nil, errHTTPTooManyRequestsLimitEmails.With(t).RetryAfter(d.RetryAfter)
|
||||
}
|
||||
}
|
||||
if call != "" {
|
||||
@@ -1122,8 +1127,8 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*model.Mess
|
||||
call, httpErr = s.convertPhoneNumber(v.User(), call)
|
||||
if httpErr != nil {
|
||||
return nil, httpErr.With(t)
|
||||
} else if !vrate.CallAllowed() {
|
||||
return nil, errHTTPTooManyRequestsLimitCalls.With(t)
|
||||
} else if d := vrate.CallAllowed(); !d.OK {
|
||||
return nil, errHTTPTooManyRequestsLimitCalls.With(t).RetryAfter(d.RetryAfter)
|
||||
}
|
||||
}
|
||||
if m.PollID != "" {
|
||||
@@ -1253,8 +1258,10 @@ func (s *Server) handleActionMessage(w http.ResponseWriter, r *http.Request, v *
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() {
|
||||
return errHTTPTooManyRequestsLimitMessages.With(t)
|
||||
if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) {
|
||||
if d := vrate.MessageAllowed(); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitMessages.With(t).RetryAfter(d.RetryAfter)
|
||||
}
|
||||
}
|
||||
sequenceID, e := s.sequenceIDFromPath(r.URL.Path)
|
||||
if e != nil {
|
||||
@@ -1694,8 +1701,10 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
|
||||
// Charge the encoded length, i.e. what actually goes over the wire. Charge before writing,
|
||||
// so an exhausted budget fails the first message and surfaces as a clean 429 with nothing
|
||||
// written.
|
||||
if meterPollBandwidth && !v.BandwidthAllowed(int64(len(encoded))) {
|
||||
return errHTTPTooManyRequestsLimitAttachmentBandwidth
|
||||
if meterPollBandwidth {
|
||||
if d := v.BandwidthAllowed(int64(len(encoded))); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitAttachmentBandwidth.RetryAfter(d.RetryAfter)
|
||||
}
|
||||
}
|
||||
wlock.Lock()
|
||||
defer wlock.Unlock()
|
||||
@@ -2137,8 +2146,10 @@ func (s *Server) topicsFromIDs(v *visitor, ids ...string) ([]*topic, error) {
|
||||
if len(s.topics) >= s.config.TotalTopicLimit {
|
||||
return nil, errHTTPTooManyRequestsLimitTotalTopics
|
||||
}
|
||||
if v != nil && !v.TopicCreationAllowed() {
|
||||
return nil, errHTTPTooManyRequestsLimitTopicCreation
|
||||
if v != nil {
|
||||
if d := v.TopicCreationAllowed(); !d.OK {
|
||||
return nil, errHTTPTooManyRequestsLimitTopicCreation.RetryAfter(d.RetryAfter)
|
||||
}
|
||||
}
|
||||
s.topics[id] = newTopic(id)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
|
||||
} else if u != nil {
|
||||
return errHTTPUnauthorized // Cannot create account from user context
|
||||
}
|
||||
if !v.AccountActionAllowed() {
|
||||
return errHTTPTooManyRequestsLimitAccountActions
|
||||
if d := v.AccountActionAllowed(); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitAccountActions.RetryAfter(d.RetryAfter)
|
||||
}
|
||||
}
|
||||
newAccount, err := readJSONWithLimit[apiAccountCreateRequest](r.Body, jsonBodyBytesLimit, false)
|
||||
@@ -710,8 +710,8 @@ func (s *Server) handleAccountEmailAdd(w http.ResponseWriter, r *http.Request, v
|
||||
return errHTTPConflictEmailExists
|
||||
}
|
||||
// Rate limit (counts against the user's email quota)
|
||||
if !v.EmailAllowed() {
|
||||
return errHTTPTooManyRequestsLimitEmails
|
||||
if d := v.EmailAllowed(); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitEmails.RetryAfter(d.RetryAfter)
|
||||
}
|
||||
logvr(v, r).Tag(tagAccount).Field("email", req.Email).Info("Starting email verification")
|
||||
if err := s.enqueueEmailVerification(u.ID, req.Email); err != nil {
|
||||
@@ -803,8 +803,8 @@ func (s *Server) handleAccountEmailResend(w http.ResponseWriter, r *http.Request
|
||||
} else if !util.Contains(pending, req.Email) {
|
||||
return errHTTPBadRequestEmailAddressInvalid
|
||||
}
|
||||
if !v.EmailAllowed() {
|
||||
return errHTTPTooManyRequestsLimitEmails
|
||||
if d := v.EmailAllowed(); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitEmails.RetryAfter(d.RetryAfter)
|
||||
}
|
||||
logvr(v, r).Tag(tagAccount).Field("email", req.Email).Info("Resending email verification")
|
||||
if err := s.enqueueEmailVerification(u.ID, req.Email); err != nil {
|
||||
@@ -838,8 +838,8 @@ func (s *Server) handleAccountPasswordResetRequest(w http.ResponseWriter, r *htt
|
||||
return err
|
||||
}
|
||||
// Rate limit via the shared per-visitor account-creation bucket (no new limiter/config)
|
||||
if !v.AccountActionAllowed() {
|
||||
return errHTTPTooManyRequestsLimitAccountActions
|
||||
if d := v.AccountActionAllowed(); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitAccountActions.RetryAfter(d.RetryAfter)
|
||||
}
|
||||
v.AccountActionPerformed() // Consume a token on every request (including no-match), to throttle probing
|
||||
identifier := strings.TrimSpace(req.Identifier)
|
||||
|
||||
@@ -142,6 +142,9 @@ func TestAccount_Signup_LimitReached(t *testing.T) {
|
||||
rr := request(t, s, "POST", "/v1/account", `{"username":"thiswontwork", "password":"mypass"}`, nil)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
require.Equal(t, 42906, toHTTPError(t, rr.Body.String()).Code)
|
||||
retryAfter := retryAfterSeconds(t, rr) // One token refills per DefaultVisitorAccountCreationLimitReplenish
|
||||
require.Greater(t, retryAfter, int(DefaultVisitorAccountCreationLimitReplenish.Seconds())-60)
|
||||
require.LessOrEqual(t, retryAfter, int(DefaultVisitorAccountCreationLimitReplenish.Seconds()))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@ func (s *Server) maybeAuthenticate(r *http.Request) (*http.Request, *visitor, er
|
||||
return r, vip, nil
|
||||
}
|
||||
// If we're trying to auth, check the rate limiter first
|
||||
if !vip.AuthAllowed() {
|
||||
return r, vip, errHTTPTooManyRequestsLimitAuthFailure // Always return visitor, even when error occurs!
|
||||
if d := vip.AuthAllowed(); !d.OK {
|
||||
return r, vip, errHTTPTooManyRequestsLimitAuthFailure.RetryAfter(d.RetryAfter) // Always return visitor, even when error occurs!
|
||||
}
|
||||
u, err := s.authenticate(r, header)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,8 +21,8 @@ func (s *Server) limitRequests(next handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
if util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) {
|
||||
return next(w, r, v)
|
||||
} else if !v.RequestAllowed() {
|
||||
return errHTTPTooManyRequestsLimitRequests
|
||||
} else if d := v.RequestAllowed(); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitRequests.RetryAfter(d.RetryAfter)
|
||||
}
|
||||
return next(w, r, v)
|
||||
}
|
||||
@@ -45,8 +45,8 @@ func (s *Server) limitRequestsWithTopic(next handleFunc) handleFunc {
|
||||
})
|
||||
if util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) {
|
||||
return next(w, r, v)
|
||||
} else if !vrate.RequestAllowed() {
|
||||
return errHTTPTooManyRequestsLimitRequests
|
||||
} else if d := vrate.RequestAllowed(); !d.OK {
|
||||
return errHTTPTooManyRequestsLimitRequests.RetryAfter(d.RetryAfter)
|
||||
}
|
||||
return next(w, r, v)
|
||||
}
|
||||
|
||||
+125
-1
@@ -3619,7 +3619,7 @@ func TestServer_SubscriberRateLimiting_StreamDoesNotReclaimTopicAfterWriteRevoke
|
||||
return to != nil && to.SubscribersCount() == 1
|
||||
})
|
||||
to := lockedTopic(s, "upAAAAAAAAAAAA")
|
||||
require.True(t, to.RateVisitor().MessageAllowed())
|
||||
require.True(t, to.RateVisitor().MessageAllowed().OK)
|
||||
require.Nil(t, s.userManager.AllowAccess("alice", "upAAAAAAAAAAAA", user.PermissionRead))
|
||||
|
||||
// Bob takes the topic with a poll, and is gone a day later
|
||||
@@ -5677,6 +5677,130 @@ func TestServer_HandleError_SkipsWriteHeaderOnHijackedConnection(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_HandleError_RetryAfter(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, databaseURL string) {
|
||||
// A 429 with a known wait carries Retry-After in whole seconds, rounded up; one without
|
||||
// (e.g. the subscription gauge) carries none
|
||||
s := newTestServer(t, newTestConfig(t, databaseURL))
|
||||
r, _ := http.NewRequest("GET", "/mytopic/json", nil)
|
||||
v := newVisitor(s.visitors, netip.MustParseAddr("1.2.3.4"), nil)
|
||||
rr := httptest.NewRecorder()
|
||||
s.handleError(rr, r, v, errHTTPTooManyRequestsLimitRequests.RetryAfter(1500*time.Millisecond))
|
||||
require.Equal(t, 429, rr.Code)
|
||||
require.Equal(t, "2", rr.Header().Get("Retry-After"))
|
||||
require.Equal(t, 42901, toHTTPError(t, rr.Body.String()).Code)
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
s.handleError(rr, r, v, errHTTPTooManyRequestsLimitRequests.RetryAfter(time.Millisecond))
|
||||
require.Equal(t, "1", rr.Header().Get("Retry-After"))
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
s.handleError(rr, r, v, errHTTPTooManyRequestsLimitSubscriptions)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
require.Empty(t, rr.Header().Get("Retry-After"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_RetryAfter_RequestLimit(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, databaseURL string) {
|
||||
c := newTestConfig(t, databaseURL)
|
||||
c.VisitorRequestLimitBurst = 3
|
||||
c.VisitorRequestLimitReplenish = 5 * time.Second
|
||||
s := newTestServer(t, c)
|
||||
for i := 0; i < 3; i++ {
|
||||
require.Equal(t, 200, request(t, s, "PUT", "/mytopic", "hi", nil).Code)
|
||||
}
|
||||
rr := request(t, s, "PUT", "/mytopic", "hi", nil)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
require.Equal(t, 42901, toHTTPError(t, rr.Body.String()).Code)
|
||||
retryAfter := retryAfterSeconds(t, rr)
|
||||
require.GreaterOrEqual(t, retryAfter, 1)
|
||||
require.LessOrEqual(t, retryAfter, 5)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_RetryAfter_DailyMessageLimit(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, databaseURL string) {
|
||||
c := newTestConfig(t, databaseURL)
|
||||
c.VisitorMessageDailyLimit = 2
|
||||
s := newTestServer(t, c)
|
||||
for i := 0; i < 2; i++ {
|
||||
require.Equal(t, 200, request(t, s, "PUT", "/mytopic", "hi", nil).Code)
|
||||
}
|
||||
rr := request(t, s, "PUT", "/mytopic", "hi", nil)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
require.Equal(t, 42908, toHTTPError(t, rr.Body.String()).Code)
|
||||
resetAt := s.limiter.ResetAt(s.visitor(netip.MustParseAddr("9.9.9.9"), nil).key)
|
||||
require.InDelta(t, time.Until(resetAt).Seconds(), retryAfterSeconds(t, rr), 2) // The wait is until the key's period ends
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_RetryAfter_EmailLimit(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, databaseURL string) {
|
||||
c := newTestConfig(t, databaseURL)
|
||||
c.VisitorEmailLimitBurst = 1
|
||||
c.VisitorEmailLimitReplenish = time.Hour
|
||||
s := newTestServer(t, c)
|
||||
s.mailer = &testMailer{}
|
||||
headers := map[string]string{
|
||||
"E-Mail": "test@example.com",
|
||||
}
|
||||
require.Equal(t, 200, request(t, s, "PUT", "/mytopic", "hi", headers).Code)
|
||||
rr := request(t, s, "PUT", "/mytopic", "hi", headers)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
require.Equal(t, 42902, toHTTPError(t, rr.Body.String()).Code)
|
||||
retryAfter := retryAfterSeconds(t, rr)
|
||||
require.Greater(t, retryAfter, 3500)
|
||||
require.LessOrEqual(t, retryAfter, 3600)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_RetryAfter_AuthFailureLimit(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, databaseURL string) {
|
||||
c := newTestConfigWithAuthFile(t, databaseURL)
|
||||
c.VisitorAuthFailureLimitBurst = 2
|
||||
c.VisitorAuthFailureLimitReplenish = time.Minute
|
||||
s := newTestServer(t, c)
|
||||
headers := map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
require.Equal(t, 401, request(t, s, "PUT", "/announcements", "test", headers).Code)
|
||||
}
|
||||
rr := request(t, s, "PUT", "/announcements", "test", headers)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
require.Equal(t, 42909, toHTTPError(t, rr.Body.String()).Code)
|
||||
retryAfter := retryAfterSeconds(t, rr)
|
||||
require.Greater(t, retryAfter, 50)
|
||||
require.LessOrEqual(t, retryAfter, 60)
|
||||
})
|
||||
}
|
||||
|
||||
func TestServer_RetryAfter_TopicCreationLimit(t *testing.T) {
|
||||
forEachBackend(t, func(t *testing.T, databaseURL string) {
|
||||
c := newTestConfig(t, databaseURL)
|
||||
c.VisitorTopicCreationLimitBurst = 1
|
||||
c.VisitorTopicCreationLimitReplenish = time.Minute
|
||||
s := newTestServer(t, c)
|
||||
require.Equal(t, 200, request(t, s, "PUT", "/fresh-topic-1", "hi", nil).Code)
|
||||
rr := request(t, s, "PUT", "/fresh-topic-2", "hi", nil)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
require.Equal(t, 42911, toHTTPError(t, rr.Body.String()).Code)
|
||||
retryAfter := retryAfterSeconds(t, rr)
|
||||
require.Greater(t, retryAfter, 50)
|
||||
require.LessOrEqual(t, retryAfter, 60)
|
||||
})
|
||||
}
|
||||
|
||||
func retryAfterSeconds(t *testing.T, rr *httptest.ResponseRecorder) int {
|
||||
t.Helper()
|
||||
header := rr.Header().Get("Retry-After")
|
||||
require.NotEmpty(t, header, "Retry-After header missing")
|
||||
seconds, err := strconv.Atoi(header)
|
||||
require.NoError(t, err)
|
||||
return seconds
|
||||
}
|
||||
|
||||
func TestServer_Publish_InvalidUTF8InBody(t *testing.T) {
|
||||
// All byte sequences from production logs, sent as message body
|
||||
tests := []struct {
|
||||
|
||||
+28
-27
@@ -166,11 +166,12 @@ func visitorExtendedInfoContext(info *visitorInfo) log.Context {
|
||||
|
||||
}
|
||||
|
||||
func (v *visitor) RequestAllowed() bool {
|
||||
// RequestAllowed takes a request from the visitor's bucket, if it has one; a denial says how long to wait
|
||||
func (v *visitor) RequestAllowed() limit.Decision {
|
||||
if v.limits.RequestLimitBurst <= 0 {
|
||||
return false // A bucket of nothing admits nothing, as before
|
||||
return limit.Decision{} // A bucket of nothing admits nothing, as before
|
||||
}
|
||||
return v.env.limiter.Allow(v.key, v.requestRule(), 1).OK
|
||||
return v.env.limiter.Allow(v.key, v.requestRule(), 1)
|
||||
}
|
||||
|
||||
// FirebaseAllowed returns false while Firebase sends are paused for the key (see FirebaseTemporarilyDeny)
|
||||
@@ -191,27 +192,27 @@ func (v *visitor) FirebaseTemporarilyDeny() {
|
||||
v.env.limiter.Allow(v.key, rule, 1)
|
||||
}
|
||||
|
||||
func (v *visitor) MessageAllowed() bool {
|
||||
func (v *visitor) MessageAllowed() limit.Decision {
|
||||
if v.limits.MessageLimit <= 0 {
|
||||
return false // A daily limit of zero admits nothing; the limit service reads it as "no cap"
|
||||
return limit.Decision{} // A daily limit of zero admits nothing; the limit service reads it as "no cap"
|
||||
}
|
||||
return v.env.limiter.Allow(v.key, v.messageRule(), 1).OK
|
||||
return v.env.limiter.Allow(v.key, v.messageRule(), 1)
|
||||
}
|
||||
|
||||
func (v *visitor) EmailAllowed() bool {
|
||||
func (v *visitor) EmailAllowed() limit.Decision {
|
||||
if v.limits.EmailLimitBurst <= 0 {
|
||||
return false // No email bucket
|
||||
return limit.Decision{} // No email bucket
|
||||
} else if v.limits.Basis == vis.BasisTier && v.limits.EmailLimit <= 0 {
|
||||
return false // No emails on the tier: the limit service reads a daily limit of zero as "no cap"
|
||||
return limit.Decision{} // No emails on the tier: the limit service reads a daily limit of zero as "no cap"
|
||||
}
|
||||
return v.env.limiter.Allow(v.key, v.emailRule(), 1).OK
|
||||
return v.env.limiter.Allow(v.key, v.emailRule(), 1)
|
||||
}
|
||||
|
||||
func (v *visitor) CallAllowed() bool {
|
||||
func (v *visitor) CallAllowed() limit.Decision {
|
||||
if v.limits.CallLimit <= 0 {
|
||||
return false // A daily limit of zero admits nothing (e.g. a tier without calls, verified number or not)
|
||||
return limit.Decision{} // A daily limit of zero admits nothing (e.g. a tier without calls, verified number or not)
|
||||
}
|
||||
return v.env.limiter.Allow(v.key, v.callRule(), 1).OK
|
||||
return v.env.limiter.Allow(v.key, v.callRule(), 1)
|
||||
}
|
||||
|
||||
// SubscriptionAllowed takes one of the visitor key's subscription slots on this node, if one is free.
|
||||
@@ -220,18 +221,18 @@ func (v *visitor) SubscriptionAllowed() bool {
|
||||
return v.env.subscriptions.Acquire(v.key, int64(v.env.config.VisitorSubscriptionLimit))
|
||||
}
|
||||
|
||||
// TopicCreationAllowed returns true if the visitor is allowed to cause a new topic to be
|
||||
// inserted into the server's in-memory topic map. Returns true if no limiter is configured.
|
||||
func (v *visitor) TopicCreationAllowed() bool {
|
||||
// TopicCreationAllowed returns OK if the visitor is allowed to cause a new topic to be
|
||||
// inserted into the server's in-memory topic map. Returns OK if no limiter is configured.
|
||||
func (v *visitor) TopicCreationAllowed() limit.Decision {
|
||||
if !v.topicCreationLimited() {
|
||||
return true
|
||||
return limit.Decision{OK: true}
|
||||
}
|
||||
return v.env.limiter.Allow(v.key, v.topicCreationRule(), 1).OK
|
||||
return v.env.limiter.Allow(v.key, v.topicCreationRule(), 1)
|
||||
}
|
||||
|
||||
// AuthAllowed returns true if an auth request can be attempted (a token is available)
|
||||
func (v *visitor) AuthAllowed() bool {
|
||||
return v.env.limiter.Tokens(v.key, v.authRule()) >= 1
|
||||
// AuthAllowed returns OK if an auth request can be attempted (a token is available)
|
||||
func (v *visitor) AuthAllowed() limit.Decision {
|
||||
return v.env.limiter.Check(v.key, v.authRule())
|
||||
}
|
||||
|
||||
// AuthFailed records an auth failure
|
||||
@@ -239,10 +240,10 @@ func (v *visitor) AuthFailed() {
|
||||
v.env.limiter.Allow(v.key, v.authRule(), 1)
|
||||
}
|
||||
|
||||
// AccountActionAllowed returns true if a rate-limited account action (signup or password-reset
|
||||
// AccountActionAllowed returns OK if a rate-limited account action (signup or password-reset
|
||||
// request) is currently allowed for this visitor
|
||||
func (v *visitor) AccountActionAllowed() bool {
|
||||
return v.env.limiter.Tokens(v.key, v.accountRule()) >= 1
|
||||
func (v *visitor) AccountActionAllowed() limit.Decision {
|
||||
return v.env.limiter.Check(v.key, v.accountRule())
|
||||
}
|
||||
|
||||
// AccountActionPerformed decreases the account limiter. This is to be called after a rate-limited
|
||||
@@ -251,11 +252,11 @@ func (v *visitor) AccountActionPerformed() {
|
||||
v.env.limiter.Allow(v.key, v.accountRule(), 1)
|
||||
}
|
||||
|
||||
func (v *visitor) BandwidthAllowed(bytes int64) bool {
|
||||
func (v *visitor) BandwidthAllowed(bytes int64) limit.Decision {
|
||||
if v.limits.AttachmentBandwidthLimit <= 0 {
|
||||
return false // A bandwidth limit of zero admits nothing, as before
|
||||
return limit.Decision{} // A bandwidth limit of zero admits nothing, as before
|
||||
}
|
||||
return v.env.limiter.Allow(v.key, v.bandwidthRule(), bytes).OK
|
||||
return v.env.limiter.Allow(v.key, v.bandwidthRule(), bytes)
|
||||
}
|
||||
|
||||
// RemoveSubscription gives back a slot taken by SubscriptionAllowed
|
||||
|
||||
+47
-47
@@ -27,10 +27,10 @@ func TestVisitor_TierUserDoesNotGetAnonymousUsage(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil)
|
||||
for i := 0; i < 3; i++ {
|
||||
require.True(t, v.MessageAllowed())
|
||||
require.True(t, v.MessageAllowed().OK)
|
||||
}
|
||||
vu := s.visitor(netip.MustParseAddr("9.9.9.9"), u)
|
||||
require.True(t, vu.RequestAllowed())
|
||||
require.True(t, vu.RequestAllowed().OK)
|
||||
require.Equal(t, int64(0), vu.Stats().Messages)
|
||||
require.Equal(t, int64(3), v.Stats().Messages)
|
||||
}
|
||||
@@ -47,8 +47,8 @@ func TestVisitor_BandwidthRefund(t *testing.T) {
|
||||
require.False(t, l.AllowN(200))
|
||||
require.True(t, l.AllowN(-500))
|
||||
require.True(t, l.AllowN(200))
|
||||
require.False(t, v.BandwidthAllowed(500))
|
||||
require.True(t, v.BandwidthAllowed(400))
|
||||
require.False(t, v.BandwidthAllowed(500).OK)
|
||||
require.True(t, v.BandwidthAllowed(400).OK)
|
||||
}
|
||||
|
||||
func TestVisitor_TierChangeKeepsBuckets(t *testing.T) {
|
||||
@@ -203,9 +203,9 @@ func TestVisitor_ZeroLimitsAdmitNothing(t *testing.T) {
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
v := s.visitor(netip.MustParseAddr("9.9.9.9"), u)
|
||||
require.False(t, v.MessageAllowed())
|
||||
require.False(t, v.CallAllowed())
|
||||
require.False(t, v.BandwidthAllowed(1))
|
||||
require.False(t, v.MessageAllowed().OK)
|
||||
require.False(t, v.CallAllowed().OK)
|
||||
require.False(t, v.BandwidthAllowed(1).OK)
|
||||
require.False(t, v.BandwidthLimiter().AllowN(1))
|
||||
})
|
||||
}
|
||||
@@ -231,9 +231,9 @@ func TestVisitor_UserIsPerRequest(t *testing.T) {
|
||||
require.Equal(t, u.ID, authedAgain.User().ID)
|
||||
|
||||
// Limits are still shared per IP: requests and the node-local subscription gauge
|
||||
for authed.RequestAllowed() {
|
||||
for authed.RequestAllowed().OK {
|
||||
}
|
||||
require.False(t, anon.RequestAllowed())
|
||||
require.False(t, anon.RequestAllowed().OK)
|
||||
for authed.SubscriptionAllowed() {
|
||||
}
|
||||
require.False(t, anon.SubscriptionAllowed())
|
||||
@@ -292,11 +292,11 @@ func TestVisitor_AuthLimiter_NotBypassedByConcurrentLogin(t *testing.T) {
|
||||
// The user logs in between the guesser's lookup and each failed guess
|
||||
for i := 0; i < 3; i++ {
|
||||
s.visitor(ip, u)
|
||||
require.True(t, vip.AuthAllowed())
|
||||
require.True(t, vip.AuthAllowed().OK)
|
||||
vip.AuthFailed()
|
||||
}
|
||||
s.visitor(ip, u)
|
||||
require.False(t, vip.AuthAllowed())
|
||||
require.False(t, vip.AuthAllowed().OK)
|
||||
}
|
||||
|
||||
func TestVisitor_AccountLimiter_NotDeniedByConcurrentLogin(t *testing.T) {
|
||||
@@ -310,7 +310,7 @@ func TestVisitor_AccountLimiter_NotDeniedByConcurrentLogin(t *testing.T) {
|
||||
vip := s.visitor(ip, nil)
|
||||
|
||||
s.visitor(ip, u)
|
||||
require.True(t, vip.AccountActionAllowed())
|
||||
require.True(t, vip.AccountActionAllowed().OK)
|
||||
}
|
||||
|
||||
func TestVisitor_RateVisitorKeptForExemptSubscriber(t *testing.T) {
|
||||
@@ -339,10 +339,10 @@ func TestVisitor_AuthAndAccountLimitsIgnoreUser(t *testing.T) {
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
v := s.visitor(netip.MustParseAddr("9.9.9.9"), u)
|
||||
require.True(t, v.AccountActionAllowed())
|
||||
require.True(t, v.AuthAllowed())
|
||||
require.True(t, v.AccountActionAllowed().OK)
|
||||
require.True(t, v.AuthAllowed().OK)
|
||||
v.AuthFailed()
|
||||
require.False(t, v.AuthAllowed())
|
||||
require.False(t, v.AuthAllowed().OK)
|
||||
}
|
||||
|
||||
func TestVisitor_SharedKeyCountsOnce(t *testing.T) {
|
||||
@@ -363,10 +363,10 @@ func TestVisitor_SharedKeyCountsOnce(t *testing.T) {
|
||||
v2 := s.visitor(netip.MustParseAddr("2.2.2.2"), u)
|
||||
require.Equal(t, v1.key, v2.key)
|
||||
for i := 0; i < 3; i++ {
|
||||
require.True(t, v1.MessageAllowed())
|
||||
require.True(t, v1.MessageAllowed().OK)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
require.True(t, v2.MessageAllowed())
|
||||
require.True(t, v2.MessageAllowed().OK)
|
||||
}
|
||||
require.Equal(t, int64(5), v1.Stats().Messages)
|
||||
require.Equal(t, int64(5), v2.Stats().Messages)
|
||||
@@ -381,9 +381,9 @@ func TestVisitor_SharedKeyCountsOnce(t *testing.T) {
|
||||
require.Equal(t, int64(5), account.Stats.Messages)
|
||||
require.Equal(t, int64(5), account.Stats.MessagesRemaining)
|
||||
for i := 0; i < 5; i++ {
|
||||
require.True(t, v2.MessageAllowed())
|
||||
require.True(t, v2.MessageAllowed().OK)
|
||||
}
|
||||
require.False(t, v1.MessageAllowed())
|
||||
require.False(t, v1.MessageAllowed().OK)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -403,8 +403,8 @@ func TestVisitor_RequestLimiterZeroBurstAdmitsNothing(t *testing.T) {
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
ip := netip.MustParseAddr("9.9.9.9")
|
||||
require.False(t, s.visitor(ip, nil).RequestAllowed())
|
||||
require.False(t, s.visitor(ip, u).RequestAllowed())
|
||||
require.False(t, s.visitor(ip, nil).RequestAllowed().OK)
|
||||
require.False(t, s.visitor(ip, u).RequestAllowed().OK)
|
||||
}
|
||||
|
||||
func TestVisitor_TopicCreationLimiterDisabledByZero(t *testing.T) {
|
||||
@@ -417,9 +417,9 @@ func TestVisitor_TopicCreationLimiterDisabledByZero(t *testing.T) {
|
||||
s := newTestServer(t, c)
|
||||
v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil)
|
||||
for i := 0; i < 3; i++ {
|
||||
require.True(t, v.TopicCreationAllowed())
|
||||
require.True(t, v.TopicCreationAllowed().OK)
|
||||
}
|
||||
return v.TopicCreationAllowed() // The 4th: denied only if a 3-burst limiter is on
|
||||
return v.TopicCreationAllowed().OK // The 4th: denied only if a 3-burst limiter is on
|
||||
}
|
||||
require.True(t, topicsAllowed(0, time.Hour))
|
||||
require.True(t, topicsAllowed(3, 0))
|
||||
@@ -447,7 +447,7 @@ func TestVisitor_TierChangeDoesNotReseedCounts(t *testing.T) {
|
||||
ip := netip.MustParseAddr("9.9.9.9")
|
||||
v := s.visitor(ip, u)
|
||||
for i := 0; i < 4; i++ {
|
||||
require.True(t, v.MessageAllowed())
|
||||
require.True(t, v.MessageAllowed().OK)
|
||||
}
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "large"))
|
||||
u, err = s.userManager.User("phil")
|
||||
@@ -455,12 +455,12 @@ func TestVisitor_TierChangeDoesNotReseedCounts(t *testing.T) {
|
||||
v = s.visitor(ip, u) // The next request carries the new tier
|
||||
require.Equal(t, int64(4), v.Stats().Messages)
|
||||
for i := 0; i < 6; i++ {
|
||||
require.True(t, v.MessageAllowed())
|
||||
require.True(t, v.MessageAllowed().OK)
|
||||
}
|
||||
require.False(t, v.MessageAllowed())
|
||||
require.False(t, v.MessageAllowed().OK)
|
||||
fresh := s.visitor(ip, u)
|
||||
require.Equal(t, int64(10), fresh.Stats().Messages)
|
||||
require.False(t, fresh.MessageAllowed())
|
||||
require.False(t, fresh.MessageAllowed().OK)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -475,17 +475,17 @@ func TestVisitor_IPPrefixKeying(t *testing.T) {
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
v := s.visitor(netip.MustParseAddr("1.2.3.4"), nil)
|
||||
require.True(t, v.MessageAllowed())
|
||||
require.True(t, v.MessageAllowed())
|
||||
require.True(t, v.MessageAllowed().OK)
|
||||
require.True(t, v.MessageAllowed().OK)
|
||||
peer := s.visitor(netip.MustParseAddr("1.2.3.200"), nil)
|
||||
require.Equal(t, v.key, peer.key)
|
||||
require.Equal(t, int64(2), peer.Stats().Messages)
|
||||
require.True(t, s.visitor(netip.MustParseAddr("1.2.4.1"), nil).MessageAllowed())
|
||||
require.True(t, s.visitor(netip.MustParseAddr("1.2.4.1"), nil).MessageAllowed().OK)
|
||||
vu := s.visitor(netip.MustParseAddr("1.2.3.77"), u)
|
||||
require.Equal(t, v.key, vu.key)
|
||||
require.True(t, vu.MessageAllowed())
|
||||
require.False(t, vu.MessageAllowed())
|
||||
require.False(t, s.visitor(netip.MustParseAddr("1.2.3.4"), nil).MessageAllowed())
|
||||
require.True(t, vu.MessageAllowed().OK)
|
||||
require.False(t, vu.MessageAllowed().OK)
|
||||
require.False(t, s.visitor(netip.MustParseAddr("1.2.3.4"), nil).MessageAllowed().OK)
|
||||
|
||||
// IPv6: the default /64
|
||||
v6 := s.visitor(netip.MustParseAddr("2001:db8:1:2::1"), nil)
|
||||
@@ -507,8 +507,8 @@ func TestVisitor_TierDowngradeReturnsToIPBucket(t *testing.T) {
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
ip := netip.MustParseAddr("9.9.9.9")
|
||||
anon := s.visitor(ip, nil)
|
||||
require.True(t, anon.MessageAllowed())
|
||||
require.True(t, anon.MessageAllowed())
|
||||
require.True(t, anon.MessageAllowed().OK)
|
||||
require.True(t, anon.MessageAllowed().OK)
|
||||
|
||||
// Up: fresh counts on the user key
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
@@ -518,7 +518,7 @@ func TestVisitor_TierDowngradeReturnsToIPBucket(t *testing.T) {
|
||||
require.NotEqual(t, anon.key, vu.key)
|
||||
require.Equal(t, int64(0), vu.Stats().Messages)
|
||||
for i := 0; i < 5; i++ {
|
||||
require.True(t, vu.MessageAllowed())
|
||||
require.True(t, vu.MessageAllowed().OK)
|
||||
}
|
||||
|
||||
// Down: back on the IP key, with the anonymous usage
|
||||
@@ -529,8 +529,8 @@ func TestVisitor_TierDowngradeReturnsToIPBucket(t *testing.T) {
|
||||
vd := s.visitor(ip, u)
|
||||
require.Equal(t, anon.key, vd.key)
|
||||
require.Equal(t, int64(2), vd.Stats().Messages)
|
||||
require.True(t, vd.MessageAllowed())
|
||||
require.False(t, vd.MessageAllowed())
|
||||
require.True(t, vd.MessageAllowed().OK)
|
||||
require.False(t, vd.MessageAllowed().OK)
|
||||
|
||||
// Up again: what was spent on the tier stays spent
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
@@ -569,7 +569,7 @@ func TestVisitor_ConcurrentSharedKey(t *testing.T) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < iterations; i++ {
|
||||
v := s.visitor(ip, u)
|
||||
if v.MessageAllowed() {
|
||||
if v.MessageAllowed().OK {
|
||||
admitted.Add(1)
|
||||
}
|
||||
v.RequestAllowed()
|
||||
@@ -611,7 +611,7 @@ func TestVisitor_BandwidthReaderRollback(t *testing.T) {
|
||||
require.ErrorIs(t, err, util.ErrLimitReached)
|
||||
require.Zero(t, n)
|
||||
require.Zero(t, l.Value())
|
||||
require.True(t, v.BandwidthAllowed(10))
|
||||
require.True(t, v.BandwidthAllowed(10).OK)
|
||||
require.False(t, l.Allow())
|
||||
}
|
||||
|
||||
@@ -663,9 +663,9 @@ func TestVisitor_EmailsWithoutTierHaveDailyCap(t *testing.T) {
|
||||
s := newTestServer(t, c)
|
||||
v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil)
|
||||
for i := 0; i < 24; i++ {
|
||||
require.True(t, v.EmailAllowed(), "email %d", i)
|
||||
require.True(t, v.EmailAllowed().OK, "email %d", i)
|
||||
}
|
||||
require.False(t, v.EmailAllowed())
|
||||
require.False(t, v.EmailAllowed().OK)
|
||||
}
|
||||
|
||||
func TestVisitor_EmailsWithoutTierAllowedWithSlowRefill(t *testing.T) {
|
||||
@@ -676,9 +676,9 @@ func TestVisitor_EmailsWithoutTierAllowedWithSlowRefill(t *testing.T) {
|
||||
c.VisitorEmailLimitReplenish = 48 * time.Hour
|
||||
s := newTestServer(t, c)
|
||||
v := s.visitor(netip.MustParseAddr("9.9.9.9"), nil)
|
||||
require.True(t, v.EmailAllowed())
|
||||
require.True(t, v.EmailAllowed())
|
||||
require.False(t, v.EmailAllowed())
|
||||
require.True(t, v.EmailAllowed().OK)
|
||||
require.True(t, v.EmailAllowed().OK)
|
||||
require.False(t, v.EmailAllowed().OK)
|
||||
}
|
||||
|
||||
func TestVisitor_LoggedInUserWrittenFromFirstMessage(t *testing.T) {
|
||||
@@ -690,8 +690,8 @@ func TestVisitor_LoggedInUserWrittenFromFirstMessage(t *testing.T) {
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
u, err := s.userManager.User("ben")
|
||||
require.Nil(t, err)
|
||||
require.True(t, s.visitor(netip.MustParseAddr("1.1.1.1"), u).MessageAllowed())
|
||||
require.True(t, s.visitor(netip.MustParseAddr("2.2.2.2"), nil).MessageAllowed())
|
||||
require.True(t, s.visitor(netip.MustParseAddr("1.1.1.1"), u).MessageAllowed().OK)
|
||||
require.True(t, s.visitor(netip.MustParseAddr("2.2.2.2"), nil).MessageAllowed().OK)
|
||||
s.Stop()
|
||||
s = newTestServer(t, c)
|
||||
require.Equal(t, int64(1), s.visitor(netip.MustParseAddr("1.1.1.1"), u).Stats().Messages)
|
||||
|
||||
Reference in new issue
Block a user