فهرست منبع

Reject reservation limits in endpoint

binwiederhier 3 سال پیش
والد
کامیت
a51d95743a
4فایلهای تغییر یافته به همراه47 افزوده شده و 9 حذف شده
  1. 3 2
      server/errors.go
  2. 11 6
      server/server.go
  3. 11 1
      server/server_account.go
  4. 22 0
      user/manager.go

+ 3 - 2
server/errors.go

@@ -69,8 +69,9 @@ var (
 	errHTTPTooManyRequestsLimitEmails                = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, 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"}
-	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
+	errHTTPTooManyRequestsLimitAttachmentBandwidth   = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth", "https://ntfy.sh/docs/publish/#limitations"}
+	errHTTPTooManyRequestsLimitAccountCreation       = &errHTTP{42906, http.StatusTooManyRequests, "limit reached: too many accounts created", "https://ntfy.sh/docs/publish/#limitations"} // FIXME document limit
+	errHTTPTooManyRequestsLimitReservations          = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", ""}
 	errHTTPInternalError                             = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""}
 	errHTTPInternalErrorInvalidPath                  = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", ""}
 	errHTTPInternalErrorMissingBaseURL               = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"}

+ 11 - 6
server/server.go

@@ -36,26 +36,31 @@ import (
 
 /*
 	TODO
-		limits:
+		limits & rate limiting:
 			message cache duration
 			Keep 10000 messages or keep X days?
 			Attachment expiration based on plan
+			login/account endpoints
 		plan:
 			weirdness with admin and "default" account
-		"account topic" sync mechanism
 		v.Info() endpoint double selects from DB
-		JS constants
 		purge accounts that were not logged into in X
 		reset daily limits for users
+		Make sure account endpoints make sense for admins
 		UI:
 		- flicker of upgrade banner
+		- JS constants
+		- useContext for account
 		Sync:
+			- "account topic" sync mechanism
 			- "mute" setting
 			- figure out what settings are "web" or "phone"
-		rate limiting:
-		- login/account endpoints
 		Tests:
+		- /access endpoints
 		- visitor with/without user
+		Refactor:
+		- rename TopicsLimit -> ReservationsLimit
+		- rename /access -> /reservation
 		Later:
 		- Password reset
 		- Pricing
@@ -496,7 +501,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
 	}
 	if r.Method == http.MethodGet {
 		if err := v.BandwidthLimiter().Allow(stat.Size()); err != nil {
-			return errHTTPTooManyRequestsAttachmentBandwidthLimit
+			return errHTTPTooManyRequestsLimitAttachmentBandwidth
 		}
 	}
 	w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))

+ 11 - 1
server/server_account.go

@@ -2,6 +2,7 @@ package server
 
 import (
 	"encoding/json"
+	"errors"
 	"heckel.io/ntfy/user"
 	"heckel.io/ntfy/util"
 	"net/http"
@@ -29,7 +30,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
 		return errHTTPConflictUserExists
 	}
 	if v.accountLimiter != nil && !v.accountLimiter.Allow() {
-		return errHTTPTooManyRequestsAccountCreateLimit
+		return errHTTPTooManyRequestsLimitAccountCreation
 	}
 	if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User
 		return err
@@ -331,6 +332,15 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request,
 	if !topicRegex.MatchString(req.Topic) {
 		return errHTTPBadRequestTopicInvalid
 	}
+	if v.user.Plan == nil {
+		return errors.New("no plan") // FIXME there should always be a plan!
+	}
+	reservations, err := s.userManager.ReservationsCount(v.user.Name)
+	if err != nil {
+		return err
+	} else if reservations >= v.user.Plan.TopicsLimit {
+		return errHTTPTooManyRequestsLimitReservations // FIXME test this
+	}
 	if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil {
 		return errHTTPConflictTopicReserved
 	}

+ 22 - 0
user/manager.go

@@ -140,6 +140,11 @@ const (
 		  AND a_user.owner_user_id = (SELECT id FROM user WHERE user = ?)
 		ORDER BY a_user.topic
 	`
+	selectUserReservationsCountQuery = `
+		SELECT COUNT(*)
+		FROM user_access
+		WHERE user_id = owner_user_id AND owner_user_id = (SELECT id FROM user WHERE user = ?)
+	`
 	selectOtherAccessCountQuery = `
 		SELECT COUNT(*)
 		FROM user_access
@@ -599,6 +604,23 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) {
 	return reservations, nil
 }
 
+// ReservationsCount returns the number of reservations owned by this user
+func (a *Manager) ReservationsCount(username string) (int64, error) {
+	rows, err := a.db.Query(selectUserReservationsCountQuery, username)
+	if err != nil {
+		return 0, err
+	}
+	defer rows.Close()
+	if !rows.Next() {
+		return 0, errNoRows
+	}
+	var count int64
+	if err := rows.Scan(&count); err != nil {
+		return 0, err
+	}
+	return count, nil
+}
+
 // ChangePassword changes a user's password
 func (a *Manager) ChangePassword(username, password string) error {
 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)