Sign up rate limit
This commit is contained in:
parent
7bd1c6e115
commit
fb470eec79
7 changed files with 34 additions and 10 deletions
|
@ -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: "",
|
||||||
|
|
|
@ -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/"}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue