From 84785b7a60871c300ffeb8d388e44f71706f7b4f Mon Sep 17 00:00:00 2001 From: binwiederhier Date: Mon, 19 Dec 2022 16:22:13 -0500 Subject: [PATCH] Restructure limits --- auth/auth.go | 2 +- auth/auth_sqlite.go | 4 +- server/server.go | 4 +- server/server_account.go | 55 ++++++++++------------------ server/types.go | 25 +++++++------ server/visitor.go | 69 +++++++++++++++++++++++++++++------ web/src/components/Account.js | 20 +++++----- 7 files changed, 105 insertions(+), 74 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 0174b170..b9e24cb9 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -84,7 +84,7 @@ const ( type Plan struct { Code string `json:"name"` Upgradable bool `json:"upgradable"` - MessageLimit int64 `json:"messages_limit"` + MessagesLimit int64 `json:"messages_limit"` EmailsLimit int64 `json:"emails_limit"` AttachmentFileSizeLimit int64 `json:"attachment_file_size_limit"` AttachmentTotalSizeLimit int64 `json:"attachment_total_size_limit"` diff --git a/auth/auth_sqlite.go b/auth/auth_sqlite.go index 60288cc2..1ebe3f3f 100644 --- a/auth/auth_sqlite.go +++ b/auth/auth_sqlite.go @@ -110,7 +110,7 @@ const ( selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` ) -// SQLiteAuthManager is an implementation of Manager and Manager. It stores users and access control list +// SQLiteAuthManager is an implementation of Manager. It stores users and access control list // in a SQLite database. type SQLiteAuthManager struct { db *sql.DB @@ -355,7 +355,7 @@ func (a *SQLiteAuthManager) readUser(rows *sql.Rows) (*User, error) { user.Plan = &Plan{ Code: planCode.String, Upgradable: true, // FIXME - MessageLimit: messagesLimit.Int64, + MessagesLimit: messagesLimit.Int64, EmailsLimit: emailsLimit.Int64, AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64, AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64, diff --git a/server/server.go b/server/server.go index 11ec614a..da168f35 100644 --- a/server/server.go +++ b/server/server.go @@ -773,7 +773,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, contentLengthStr := r.Header.Get("Content-Length") if contentLengthStr != "" { // Early "do-not-trust" check, hard limit see below contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64) - if err == nil && (contentLength > visitorStats.VisitorAttachmentBytesRemaining || contentLength > s.config.AttachmentFileSizeLimit) { + if err == nil && (contentLength > visitorStats.AttachmentTotalSizeRemaining || contentLength > s.config.AttachmentFileSizeLimit) { return errHTTPEntityTooLargeAttachmentTooLarge } } @@ -791,7 +791,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, if m.Message == "" { m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name) } - m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(visitorStats.VisitorAttachmentBytesRemaining)) + m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.BandwidthLimiter(), util.NewFixedLimiter(visitorStats.AttachmentTotalSizeRemaining)) if err == util.ErrLimitReached { return errHTTPEntityTooLargeAttachmentTooLarge } else if err != nil { diff --git a/server/server_account.go b/server/server_account.go index 6078c587..65e911ba 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -40,7 +40,21 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis return err } response := &apiAccountSettingsResponse{ - Usage: &apiAccountStats{}, + Stats: &apiAccountStats{ + Messages: stats.Messages, + MessagesRemaining: stats.MessagesRemaining, + Emails: stats.Emails, + EmailsRemaining: stats.EmailsRemaining, + AttachmentTotalSize: stats.AttachmentTotalSize, + AttachmentTotalSizeRemaining: stats.AttachmentTotalSizeRemaining, + }, + Limits: &apiAccountLimits{ + Basis: stats.Basis, + Messages: stats.MessagesLimit, + Emails: stats.EmailsLimit, + AttachmentTotalSize: stats.AttachmentTotalSizeLimit, + AttachmentFileSize: stats.AttachmentFileSizeLimit, + }, } if v.user != nil { response.Username = v.user.Name @@ -57,62 +71,31 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis } } if v.user.Plan != nil { - response.Usage.Basis = "account" - response.Plan = &apiAccountSettingsPlan{ + response.Plan = &apiAccountPlan{ Code: v.user.Plan.Code, Upgradable: v.user.Plan.Upgradable, } - response.Limits = &apiAccountLimits{ - MessagesLimit: v.user.Plan.MessageLimit, - EmailsLimit: v.user.Plan.EmailsLimit, - AttachmentFileSizeLimit: v.user.Plan.AttachmentFileSizeLimit, - AttachmentTotalSizeLimit: v.user.Plan.AttachmentTotalSizeLimit, - } } else { if v.user.Role == auth.RoleAdmin { - response.Usage.Basis = "account" - response.Plan = &apiAccountSettingsPlan{ + response.Plan = &apiAccountPlan{ Code: string(auth.PlanUnlimited), Upgradable: false, } - response.Limits = &apiAccountLimits{ - MessagesLimit: 0, - EmailsLimit: 0, - AttachmentFileSizeLimit: 0, - AttachmentTotalSizeLimit: 0, - } } else { - response.Usage.Basis = "ip" - response.Plan = &apiAccountSettingsPlan{ + response.Plan = &apiAccountPlan{ Code: string(auth.PlanDefault), Upgradable: true, } - response.Limits = &apiAccountLimits{ - MessagesLimit: int64(s.config.VisitorRequestLimitBurst), - EmailsLimit: int64(s.config.VisitorEmailLimitBurst), - AttachmentFileSizeLimit: s.config.AttachmentFileSizeLimit, - AttachmentTotalSizeLimit: s.config.VisitorAttachmentTotalSizeLimit, - } } } } else { response.Username = auth.Everyone response.Role = string(auth.RoleAnonymous) - response.Usage.Basis = "ip" - response.Plan = &apiAccountSettingsPlan{ + response.Plan = &apiAccountPlan{ Code: string(auth.PlanNone), Upgradable: true, } - response.Limits = &apiAccountLimits{ - MessagesLimit: int64(s.config.VisitorRequestLimitBurst), - EmailsLimit: int64(s.config.VisitorEmailLimitBurst), - AttachmentFileSizeLimit: s.config.AttachmentFileSizeLimit, - AttachmentTotalSizeLimit: s.config.VisitorAttachmentTotalSizeLimit, - } } - response.Usage.Messages = stats.Messages - response.Usage.Emails = stats.Emails - response.Usage.AttachmentsSize = stats.AttachmentBytes if err := json.NewEncoder(w).Encode(response); err != nil { return err } diff --git a/server/types.go b/server/types.go index bc9718f4..8cdfc43c 100644 --- a/server/types.go +++ b/server/types.go @@ -224,23 +224,26 @@ type apiAccountTokenResponse struct { Token string `json:"token"` } -type apiAccountSettingsPlan struct { +type apiAccountPlan struct { Code string `json:"code"` Upgradable bool `json:"upgradable"` } type apiAccountLimits struct { - MessagesLimit int64 `json:"messages"` - EmailsLimit int64 `json:"emails"` - AttachmentFileSizeLimit int64 `json:"attachment_file_size"` - AttachmentTotalSizeLimit int64 `json:"attachment_total_size"` + Basis string `json:"basis"` // "ip", "role" or "plan" + Messages int64 `json:"messages"` + Emails int64 `json:"emails"` + AttachmentTotalSize int64 `json:"attachment_total_size"` + AttachmentFileSize int64 `json:"attachment_file_size"` } type apiAccountStats struct { - Basis string `json:"basis"` // "ip" or "account" - Messages int64 `json:"messages"` - Emails int64 `json:"emails"` - AttachmentsSize int64 `json:"attachments_size"` + Messages int64 `json:"messages"` + MessagesRemaining int64 `json:"messages_remaining"` + Emails int64 `json:"emails"` + EmailsRemaining int64 `json:"emails_remaining"` + AttachmentTotalSize int64 `json:"attachment_total_size"` + AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"` } type apiAccountSettingsResponse struct { @@ -249,7 +252,7 @@ type apiAccountSettingsResponse struct { Language string `json:"language,omitempty"` Notification *auth.UserNotificationPrefs `json:"notification,omitempty"` Subscriptions []*auth.UserSubscription `json:"subscriptions,omitempty"` - Plan *apiAccountSettingsPlan `json:"plan,omitempty"` + Plan *apiAccountPlan `json:"plan,omitempty"` Limits *apiAccountLimits `json:"limits,omitempty"` - Usage *apiAccountStats `json:"usage,omitempty"` + Stats *apiAccountStats `json:"stats,omitempty"` } diff --git a/server/visitor.go b/server/visitor.go index 36b68829..0e81c0f9 100644 --- a/server/visitor.go +++ b/server/visitor.go @@ -40,17 +40,27 @@ type visitor struct { } type visitorStats struct { - Messages int64 - Emails int64 - AttachmentBytes int64 + Basis string // "ip", "role" or "plan" + Messages int64 + MessagesLimit int64 + MessagesRemaining int64 + Emails int64 + EmailsLimit int64 + EmailsRemaining int64 + AttachmentTotalSize int64 + AttachmentTotalSizeLimit int64 + AttachmentTotalSizeRemaining int64 + AttachmentFileSizeLimit int64 } func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor { - var requestLimiter *rate.Limiter + var requestLimiter, emailsLimiter *rate.Limiter if user != nil && user.Plan != nil { - requestLimiter = rate.NewLimiter(rate.Limit(user.Plan.MessageLimit)*rate.Every(24*time.Hour), conf.VisitorRequestLimitBurst) + requestLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.MessagesLimit), conf.VisitorRequestLimitBurst) + emailsLimiter = rate.NewLimiter(dailyLimitToRate(user.Plan.EmailsLimit), conf.VisitorEmailLimitBurst) } else { requestLimiter = rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst) + emailsLimiter = rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst) } return &visitor{ config: conf, @@ -60,7 +70,7 @@ func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *a messages: 0, // TODO emails: 0, // TODO requestLimiter: requestLimiter, - emailsLimiter: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst), + emailsLimiter: emailsLimiter, subscriptionLimiter: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)), bandwidthLimiter: util.NewBytesLimiter(conf.VisitorAttachmentDailyBandwidthLimit, 24*time.Hour), firebase: time.Unix(0, 0), @@ -147,9 +157,46 @@ func (v *visitor) Stats() (*visitorStats, error) { } v.mu.Lock() defer v.mu.Unlock() - return &visitorStats{ - Messages: v.messages, - Emails: v.emails, - AttachmentBytes: attachmentsBytesUsed, - }, nil + stats := &visitorStats{} + if v.user != nil && v.user.Role == auth.RoleAdmin { + stats.Basis = "role" + stats.MessagesLimit = 0 + stats.EmailsLimit = 0 + stats.AttachmentTotalSizeLimit = 0 + stats.AttachmentFileSizeLimit = 0 + } else if v.user != nil && v.user.Plan != nil { + stats.Basis = "plan" + stats.MessagesLimit = v.user.Plan.MessagesLimit + stats.EmailsLimit = v.user.Plan.EmailsLimit + stats.AttachmentTotalSizeLimit = v.user.Plan.AttachmentTotalSizeLimit + stats.AttachmentFileSizeLimit = v.user.Plan.AttachmentFileSizeLimit + } else { + stats.Basis = "ip" + stats.MessagesLimit = replenishDurationToDailyLimit(v.config.VisitorRequestLimitReplenish) + stats.EmailsLimit = replenishDurationToDailyLimit(v.config.VisitorEmailLimitReplenish) + stats.AttachmentTotalSizeLimit = v.config.AttachmentTotalSizeLimit + stats.AttachmentFileSizeLimit = v.config.AttachmentFileSizeLimit + } + stats.Messages = v.messages + stats.MessagesRemaining = zeroIfNegative(stats.MessagesLimit - stats.MessagesLimit) + stats.Emails = v.emails + stats.EmailsRemaining = zeroIfNegative(stats.EmailsLimit - stats.EmailsRemaining) + stats.AttachmentTotalSize = attachmentsBytesUsed + stats.AttachmentTotalSizeRemaining = zeroIfNegative(stats.AttachmentTotalSizeLimit - stats.AttachmentTotalSize) + return stats, nil +} + +func zeroIfNegative(value int64) int64 { + if value < 0 { + return 0 + } + return value +} + +func replenishDurationToDailyLimit(duration time.Duration) int64 { + return int64(24 * time.Hour / duration) +} + +func dailyLimitToRate(limit int64) rate.Limit { + return rate.Limit(limit) * rate.Every(24*time.Hour) } diff --git a/web/src/components/Account.js b/web/src/components/Account.js index 1c728da7..1b4d4ea2 100644 --- a/web/src/components/Account.js +++ b/web/src/components/Account.js @@ -60,8 +60,6 @@ const Stats = () => { return <>; // TODO loading } const accountType = account.plan.code ?? "none"; - const limits = account.limits; - const usage = account.usage; const normalize = (value, max) => (value / max * 100); return ( @@ -78,24 +76,24 @@ const Stats = () => {
- {usage.messages} - {limits.messages > 0 ? t("of {{limit}}", { limit: limits.messages }) : t("Unlimited")} + {account.stats.messages} + {account.limits.messages > 0 ? t("of {{limit}}", { limit: account.limits.messages }) : t("Unlimited")}
- 0 ? normalize(usage.messages, limits.messages) : 100} /> + 0 ? normalize(account.stats.messages, account.limits.messages) : 100} />
- {usage.emails} - {limits.emails > 0 ? t("of {{limit}}", { limit: limits.emails }) : t("Unlimited")} + {account.stats.emails} + {account.limits.emails > 0 ? t("of {{limit}}", { limit: account.limits.emails }) : t("Unlimited")}
- 0 ? normalize(usage.emails, limits.emails) : 100} /> + 0 ? normalize(account.stats.emails, account.limits.emails) : 100} />
- {formatBytes(usage.attachments_size)} - {limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(limits.attachment_total_size) }) : t("Unlimited")} + {formatBytes(account.stats.attachment_total_size)} + {account.limits.attachment_total_size > 0 ? t("of {{limit}}", { limit: formatBytes(account.limits.attachment_total_size) }) : t("Unlimited")}
- 0 ? normalize(usage.attachments_size, limits.attachment_total_size) : 100} /> + 0 ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} />