binwiederhier 3 лет назад
Родитель
Сommit
fb470eec79
7 измененных файлов с 34 добавлено и 10 удалено
  1. 6 0
      server/config.go
  2. 1 0
      server/errors.go
  3. 1 1
      server/server.go
  4. 9 3
      server/server_account.go
  5. 6 2
      server/visitor.go
  6. 8 3
      web/src/app/Api.js
  7. 3 1
      web/src/components/Signup.js

+ 6 - 0
server/config.go

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

+ 1 - 0
server/errors.go

@@ -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"}
 	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"}
+	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", ""}
 	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/"}

+ 1 - 1
server/server.go

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

+ 9 - 3
server/server_account.go

@@ -9,10 +9,13 @@ import (
 )
 
 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
-	if !signupAllowed && !admin {
-		return errHTTPBadRequestSignupNotEnabled
+	if !admin {
+		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
 	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 {
 		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
 		return err
 	}

+ 6 - 2
server/visitor.go

@@ -34,7 +34,8 @@ type visitor struct {
 	emailsLimiter       *rate.Limiter // Rate limiter for emails
 	subscriptionLimiter util.Limiter  // Fixed limiter for active subscriptions (ongoing connections)
 	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
 	mu                  sync.Mutex
 }
@@ -54,11 +55,13 @@ type visitorStats struct {
 }
 
 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
 	if user != nil {
 		messages = user.Stats.Messages
 		emails = user.Stats.Emails
+	} else {
+		accountLimiter = rate.NewLimiter(rate.Every(conf.VisitorAccountCreateLimitReplenish), conf.VisitorAccountCreateLimitBurst)
 	}
 	if user != nil && user.Plan != nil {
 		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,
 		subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)),
 		bandwidthLimiter:    util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour),
+		accountLimiter:      accountLimiter, // May be nil
 		firebase:            time.Unix(0, 0),
 		seen:                time.Now(),
 	}

+ 8 - 3
web/src/app/Api.js

@@ -161,9 +161,10 @@ class Api {
             body: body
         });
         if (response.status === 409) {
-            throw new UsernameTakenError(username)
-        }
-        if (response.status !== 200) {
+            throw new UsernameTakenError(username);
+        } else if (response.status === 429) {
+            throw new AccountCreateLimitReachedError();
+        } else if (response.status !== 200) {
             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();
 export default api;

+ 3 - 1
web/src/components/Signup.js

@@ -2,7 +2,7 @@ import * as React from 'react';
 import TextField from "@mui/material/TextField";
 import Button from "@mui/material/Button";
 import Box from "@mui/material/Box";
-import api, {UsernameTakenError} from "../app/Api";
+import api, {AccountCreateLimitReachedError, UsernameTakenError} from "../app/Api";
 import routes from "./routes";
 import session from "../app/Session";
 import Typography from "@mui/material/Typography";
@@ -36,6 +36,8 @@ const Signup = () => {
             console.log(`[Signup] Signup for user ${user.username} failed`, e);
             if ((e instanceof UsernameTakenError)) {
                 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) {
                 setError(e.message);
             } else {