Sign up rate limit

This commit is contained in:
binwiederhier 2022-12-24 12:10:51 -05:00
parent 7bd1c6e115
commit fb470eec79
7 changed files with 34 additions and 10 deletions

View file

@ -44,6 +44,8 @@ const (
DefaultVisitorRequestLimitReplenish = 5 * time.Second DefaultVisitorRequestLimitReplenish = 5 * time.Second
DefaultVisitorEmailLimitBurst = 16 DefaultVisitorEmailLimitBurst = 16
DefaultVisitorEmailLimitReplenish = time.Hour DefaultVisitorEmailLimitReplenish = time.Hour
DefaultVisitorAccountCreateLimitBurst = 2
DefaultVisitorAccountCreateLimitReplenish = 24 * time.Hour
DefaultVisitorAttachmentTotalSizeLimit = 100 * 1024 * 1024 // 100 MB DefaultVisitorAttachmentTotalSizeLimit = 100 * 1024 * 1024 // 100 MB
DefaultVisitorAttachmentDailyBandwidthLimit = 500 * 1024 * 1024 // 500 MB DefaultVisitorAttachmentDailyBandwidthLimit = 500 * 1024 * 1024 // 500 MB
) )
@ -98,6 +100,8 @@ type Config struct {
VisitorRequestExemptIPAddrs []netip.Prefix VisitorRequestExemptIPAddrs []netip.Prefix
VisitorEmailLimitBurst int VisitorEmailLimitBurst int
VisitorEmailLimitReplenish time.Duration VisitorEmailLimitReplenish time.Duration
VisitorAccountCreateLimitBurst int
VisitorAccountCreateLimitReplenish time.Duration
BehindProxy bool BehindProxy bool
EnableWeb bool EnableWeb bool
EnableSignup bool EnableSignup bool
@ -147,6 +151,8 @@ func NewConfig() *Config {
VisitorRequestExemptIPAddrs: make([]netip.Prefix, 0), VisitorRequestExemptIPAddrs: make([]netip.Prefix, 0),
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst, VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish, VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
VisitorAccountCreateLimitBurst: DefaultVisitorAccountCreateLimitBurst,
VisitorAccountCreateLimitReplenish: DefaultVisitorAccountCreateLimitReplenish,
BehindProxy: false, BehindProxy: false,
EnableWeb: true, EnableWeb: true,
Version: "", Version: "",

View file

@ -65,6 +65,7 @@ var (
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
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"} 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"}
errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsAccountCreateLimit = &errHTTP{42906, http.StatusTooManyRequests, "too many requests: daily account creation limit reached", "https://ntfy.sh/docs/publish/#limitations"} // FIXME document limit
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""} errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""}
errHTTPInternalErrorInvalidFilePath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""} errHTTPInternalErrorInvalidFilePath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""}
errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"} errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"}

View file

@ -42,10 +42,10 @@ import (
expire tokens expire tokens
auto-refresh tokens from UI auto-refresh tokens from UI
reserve topics reserve topics
rate limit for signup (2 per 24h)
handle invalid session token handle invalid session token
purge accounts that were not logged into in X purge accounts that were not logged into in X
sync subscription display name sync subscription display name
reset daily limits for users
store users store users
Pages: Pages:
- Home - Home

View file

@ -9,10 +9,13 @@ import (
) )
func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *visitor) error { func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
signupAllowed := s.config.EnableSignup
admin := v.user != nil && v.user.Role == auth.RoleAdmin admin := v.user != nil && v.user.Role == auth.RoleAdmin
if !signupAllowed && !admin { if !admin {
return errHTTPBadRequestSignupNotEnabled if !s.config.EnableSignup {
return errHTTPBadRequestSignupNotEnabled
} else if v.user != nil {
return errHTTPUnauthorized // Cannot create account from user context
}
} }
body, err := util.Peek(r.Body, 4096) // FIXME body, err := util.Peek(r.Body, 4096) // FIXME
if err != nil { if err != nil {
@ -26,6 +29,9 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
if existingUser, _ := s.auth.User(newAccount.Username); existingUser != nil { if existingUser, _ := s.auth.User(newAccount.Username); existingUser != nil {
return errHTTPConflictUserExists return errHTTPConflictUserExists
} }
if v.accountLimiter != nil && !v.accountLimiter.Allow() {
return errHTTPTooManyRequestsAccountCreateLimit
}
if err := s.auth.AddUser(newAccount.Username, newAccount.Password, auth.RoleUser); err != nil { // TODO this should return a User if err := s.auth.AddUser(newAccount.Username, newAccount.Password, auth.RoleUser); err != nil { // TODO this should return a User
return err return err
} }

View file

@ -34,7 +34,8 @@ type visitor struct {
emailsLimiter *rate.Limiter // Rate limiter for emails emailsLimiter *rate.Limiter // Rate limiter for emails
subscriptionLimiter util.Limiter // Fixed limiter for active subscriptions (ongoing connections) subscriptionLimiter util.Limiter // Fixed limiter for active subscriptions (ongoing connections)
bandwidthLimiter util.Limiter bandwidthLimiter util.Limiter
firebase time.Time // Next allowed Firebase message accountLimiter *rate.Limiter // Rate limiter for account creation
firebase time.Time // Next allowed Firebase message
seen time.Time seen time.Time
mu sync.Mutex mu sync.Mutex
} }
@ -54,11 +55,13 @@ type visitorStats struct {
} }
func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor { func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor {
var requestLimiter, emailsLimiter *rate.Limiter var requestLimiter, emailsLimiter, accountLimiter *rate.Limiter
var messages, emails int64 var messages, emails int64
if user != nil { if user != nil {
messages = user.Stats.Messages messages = user.Stats.Messages
emails = user.Stats.Emails emails = user.Stats.Emails
} else {
accountLimiter = rate.NewLimiter(rate.Every(conf.VisitorAccountCreateLimitReplenish), conf.VisitorAccountCreateLimitBurst)
} }
if user != nil && user.Plan != nil { if user != nil && user.Plan != nil {
requestLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.MessagesLimit), conf.VisitorRequestLimitBurst) requestLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.MessagesLimit), conf.VisitorRequestLimitBurst)
@ -78,6 +81,7 @@ func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *a
emailsLimiter: emailsLimiter, emailsLimiter: emailsLimiter,
subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)), subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)),
bandwidthLimiter: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour), bandwidthLimiter: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour),
accountLimiter: accountLimiter, // May be nil
firebase: time.Unix(0, 0), firebase: time.Unix(0, 0),
seen: time.Now(), seen: time.Now(),
} }

View file

@ -161,9 +161,10 @@ class Api {
body: body body: body
}); });
if (response.status === 409) { if (response.status === 409) {
throw new UsernameTakenError(username) throw new UsernameTakenError(username);
} } else if (response.status === 429) {
if (response.status !== 200) { throw new AccountCreateLimitReachedError();
} else if (response.status !== 200) {
throw new Error(`Unexpected server response ${response.status}`); throw new Error(`Unexpected server response ${response.status}`);
} }
} }
@ -260,5 +261,9 @@ export class UsernameTakenError extends Error {
} }
} }
export class AccountCreateLimitReachedError extends Error {
// Nothing
}
const api = new Api(); const api = new Api();
export default api; export default api;

View file

@ -2,7 +2,7 @@ import * as React from 'react';
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import api, {UsernameTakenError} from "../app/Api"; import api, {AccountCreateLimitReachedError, UsernameTakenError} from "../app/Api";
import routes from "./routes"; import routes from "./routes";
import session from "../app/Session"; import session from "../app/Session";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
@ -36,6 +36,8 @@ const Signup = () => {
console.log(`[Signup] Signup for user ${user.username} failed`, e); console.log(`[Signup] Signup for user ${user.username} failed`, e);
if ((e instanceof UsernameTakenError)) { if ((e instanceof UsernameTakenError)) {
setError(t("Username {{username}} is already taken", { username: e.username })); setError(t("Username {{username}} is already taken", { username: e.username }));
} else if ((e instanceof AccountCreateLimitReachedError)) {
setError(t("Account creation limit reached"));
} else if (e.message) { } else if (e.message) {
setError(e.message); setError(e.message);
} else { } else {