Skip to content

Commit 36b3303

Browse files
committed
Add message-{size|delay}-limit
1 parent 17709f2 commit 36b3303

12 files changed

+210
-90
lines changed

cmd/serve.go

+92-48
Large diffs are not rendered by default.

cmd/tier.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,9 @@ func printTier(c *cli.Context, tier *user.Tier) {
366366
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
367367
fmt.Fprintf(c.App.ErrWriter, "- Phone call limit: %d\n", tier.CallLimit)
368368
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
369-
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))
370-
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit))
369+
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSizeHuman(tier.AttachmentFileSizeLimit))
370+
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSizeHuman(tier.AttachmentTotalSizeLimit))
371371
fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
372-
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit))
372+
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSizeHuman(tier.AttachmentBandwidthLimit))
373373
fmt.Fprintf(c.App.ErrWriter, "- Stripe prices (monthly/yearly): %s\n", prices)
374374
}

server/config.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ const (
1515
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
1616
DefaultManagerInterval = time.Minute
1717
DefaultDelayedSenderInterval = 10 * time.Second
18-
DefaultMinDelay = 10 * time.Second
19-
DefaultMaxDelay = 3 * 24 * time.Hour
18+
DefaultMessageDelayMin = 10 * time.Second
19+
DefaultMessageDelayMax = 3 * 24 * time.Hour
2020
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // ~control topic (Android), not too frequently to save battery
2121
DefaultFirebasePollInterval = 20 * time.Minute // ~poll topic (iOS), max. 2-3 times per hour (see docs)
2222
DefaultFirebaseQuotaExceededPenaltyDuration = 10 * time.Minute // Time that over-users are locked out of Firebase if it returns "quota exceeded"
@@ -34,7 +34,7 @@ const (
3434
// - total topic limit: max number of topics overall
3535
// - various attachment limits
3636
const (
37-
DefaultMessageLengthLimit = 4096 // Bytes
37+
DefaultMessageSizeLimit = 4096 // Bytes; note that FCM/APNS have a limit of ~4 KB for the entire message
3838
DefaultTotalTopicLimit = 15000
3939
DefaultAttachmentTotalSizeLimit = int64(5 * 1024 * 1024 * 1024) // 5 GB
4040
DefaultAttachmentFileSizeLimit = int64(15 * 1024 * 1024) // 15 MB
@@ -122,9 +122,9 @@ type Config struct {
122122
MetricsEnable bool
123123
MetricsListenHTTP string
124124
ProfileListenHTTP string
125-
MessageLimit int
126-
MinDelay time.Duration
127-
MaxDelay time.Duration
125+
MessageDelayMin time.Duration
126+
MessageDelayMax time.Duration
127+
MessageSizeLimit int
128128
TotalTopicLimit int
129129
TotalAttachmentSizeLimit int64
130130
VisitorSubscriptionLimit int
@@ -211,9 +211,9 @@ func NewConfig() *Config {
211211
TwilioPhoneNumber: "",
212212
TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests
213213
TwilioVerifyService: "",
214-
MessageLimit: DefaultMessageLengthLimit,
215-
MinDelay: DefaultMinDelay,
216-
MaxDelay: DefaultMaxDelay,
214+
MessageSizeLimit: DefaultMessageSizeLimit,
215+
MessageDelayMin: DefaultMessageDelayMin,
216+
MessageDelayMax: DefaultMessageDelayMax,
217217
TotalTopicLimit: DefaultTotalTopicLimit,
218218
TotalAttachmentSizeLimit: 0,
219219
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,

server/server.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
733733
if err != nil {
734734
return nil, err
735735
}
736-
body, err := util.Peek(r.Body, s.config.MessageLimit)
736+
body, err := util.Peek(r.Body, s.config.MessageSizeLimit)
737737
if err != nil {
738738
return nil, err
739739
}
@@ -996,9 +996,9 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
996996
delay, err := util.ParseFutureTime(delayStr, time.Now())
997997
if err != nil {
998998
return false, false, "", "", false, errHTTPBadRequestDelayCannotParse
999-
} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
999+
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
10001000
return false, false, "", "", false, errHTTPBadRequestDelayTooSmall
1001-
} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
1001+
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
10021002
return false, false, "", "", false, errHTTPBadRequestDelayTooLarge
10031003
}
10041004
m.Time = delay.Unix()
@@ -1754,7 +1754,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
17541754
// before passing it on to the next handler. This is meant to be used in combination with handlePublish.
17551755
func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
17561756
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
1757-
m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageLimit*2, false) // 2x to account for JSON format overhead
1757+
m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageSizeLimit*2, false) // 2x to account for JSON format overhead
17581758
if err != nil {
17591759
return err
17601760
}
@@ -1812,7 +1812,7 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
18121812

18131813
func (s *Server) transformMatrixJSON(next handleFunc) handleFunc {
18141814
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
1815-
newRequest, err := newRequestFromMatrixJSON(r, s.config.BaseURL, s.config.MessageLimit)
1815+
newRequest, err := newRequestFromMatrixJSON(r, s.config.BaseURL, s.config.MessageSizeLimit)
18161816
if err != nil {
18171817
logvr(v, r).Tag(tagMatrix).Err(err).Debug("Invalid Matrix request")
18181818
if e, ok := err.(*errMatrixPushkeyRejected); ok {

server/server.yml

+10-6
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,16 @@
236236
# upstream-base-url:
237237
# upstream-access-token:
238238

239+
# Configures message-specific limits
240+
#
241+
# - message-size-limit defines the max size of a message body. Please note message sizes >4K are NOT RECOMMENDED,
242+
# and largely untested. If FCM and/or APNS is used, the limit should stay 4K, because their limits are around that size.
243+
# If you increase this size limit regardless, FCM and APNS will NOT work for large messages.
244+
# - message-delay-limit defines the max delay of a message when using the "Delay" header.
245+
#
246+
# message-size-limit: "4k"
247+
# message-delay-limit: "3d"
248+
239249
# Rate limiting: Total number of topics before the server rejects new topics.
240250
#
241251
# global-topic-limit: 15000
@@ -360,9 +370,3 @@
360370
# log-level-overrides:
361371
# log-format: text
362372
# log-file:
363-
364-
# Defines the size limit (in bytes) for a ntfy message.
365-
# NOTE: FCM has size limit at 4000 bytes. APNS has size limit at 4KB. If you increase this size limit, FCM and APNS will NOT work for large messages.
366-
# The default value is 4096 bytes.
367-
#
368-
# message-limit:

server/server_account_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -718,11 +718,11 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
718718
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
719719
require.Nil(t, s.userManager.AddTier(&user.Tier{
720720
Code: "starter",
721-
MessageLimit: 10,
721+
MessageSizeLimit: 10,
722722
}))
723723
require.Nil(t, s.userManager.AddTier(&user.Tier{
724724
Code: "pro",
725-
MessageLimit: 20,
725+
MessageSizeLimit: 20,
726726
}))
727727
require.Nil(t, s.userManager.ChangeTier("phil", "starter"))
728728

server/smtp_server.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ func (s *smtpSession) Data(r io.Reader) error {
150150
return err
151151
}
152152
body = strings.TrimSpace(body)
153-
if len(body) > conf.MessageLimit {
154-
body = body[:conf.MessageLimit]
153+
if len(body) > conf.MessageSizeLimit {
154+
body = body[:conf.MessageSizeLimit]
155155
}
156156
m := newDefaultMessage(s.topic, body)
157157
subject := strings.TrimSpace(msg.Header.Get("Subject"))

server/visitor.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ const (
3030
visitorDefaultCallsLimit = int64(0)
3131
)
3232

33-
// Constants used to convert a tier-user's MessageLimit (see user.Tier) into adequate request limiter
33+
// Constants used to convert a tier-user's MessageSizeLimit (see user.Tier) into adequate request limiter
3434
// values (token bucket). This is only used to increase the values in server.yml, never decrease them.
3535
//
36-
// Example: Assuming a user.Tier's MessageLimit is 10,000:
36+
// Example: Assuming a user.Tier's MessageSizeLimit is 10,000:
3737
// - the allowed burst is 500 (= 10,000 * 5%), which is < 1000 (the max)
3838
// - the replenish rate is 2 * 10,000 / 24 hours
3939
const (

util/time.go

+16
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ func ParseDuration(s string) (time.Duration, error) {
8383
return 0, errUnparsableTime
8484
}
8585

86+
func FormatDuration(d time.Duration) string {
87+
if d >= 24*time.Hour {
88+
return strconv.Itoa(int(d/(24*time.Hour))) + "d"
89+
}
90+
if d >= time.Hour {
91+
return strconv.Itoa(int(d/time.Hour)) + "h"
92+
}
93+
if d >= time.Minute {
94+
return strconv.Itoa(int(d/time.Minute)) + "m"
95+
}
96+
if d >= time.Second {
97+
return strconv.Itoa(int(d/time.Second)) + "s"
98+
}
99+
return "0s"
100+
}
101+
86102
func parseFromDuration(s string, now time.Time) (time.Time, error) {
87103
d, err := ParseDuration(s)
88104
if err == nil {

util/time_test.go

+24
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,27 @@ func TestParseDuration(t *testing.T) {
9292
require.Nil(t, err)
9393
require.Equal(t, time.Duration(0), d)
9494
}
95+
96+
func TestFormatDuration(t *testing.T) {
97+
values := []struct {
98+
duration time.Duration
99+
expected string
100+
}{
101+
{24 * time.Second, "24s"},
102+
{56 * time.Minute, "56m"},
103+
{time.Hour, "1h"},
104+
{2 * time.Hour, "2h"},
105+
{24 * time.Hour, "1d"},
106+
{3 * 24 * time.Hour, "3d"},
107+
}
108+
for _, value := range values {
109+
require.Equal(t, value.expected, FormatDuration(value.duration))
110+
d, err := ParseDuration(FormatDuration(value.duration))
111+
require.Nil(t, err)
112+
require.Equalf(t, value.duration, d, "duration does not match: %v != %v", value.duration, d)
113+
}
114+
}
115+
116+
func TestFormatDuration_Rounded(t *testing.T) {
117+
require.Equal(t, "1d", FormatDuration(47*time.Hour))
118+
}

util/util.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"io"
10+
"math"
1011
"math/rand"
1112
"net/netip"
1213
"os"
@@ -215,6 +216,8 @@ func ParseSize(s string) (int64, error) {
215216
return -1, fmt.Errorf("cannot convert number %s", matches[1])
216217
}
217218
switch strings.ToUpper(matches[2]) {
219+
case "T":
220+
return int64(value) * 1024 * 1024 * 1024 * 1024, nil
218221
case "G":
219222
return int64(value) * 1024 * 1024 * 1024, nil
220223
case "M":
@@ -226,8 +229,23 @@ func ParseSize(s string) (int64, error) {
226229
}
227230
}
228231

229-
// FormatSize formats bytes into a human-readable notation, e.g. 2.1 MB
232+
// FormatSize formats the size in a way that it can be parsed by ParseSize.
233+
// It does not include decimal places. Uneven sizes are rounded down.
230234
func FormatSize(b int64) string {
235+
const unit = 1024
236+
if b < unit {
237+
return fmt.Sprintf("%d", b)
238+
}
239+
div, exp := int64(unit), 0
240+
for n := b / unit; n >= unit; n /= unit {
241+
div *= unit
242+
exp++
243+
}
244+
return fmt.Sprintf("%d%c", int(math.Floor(float64(b)/float64(div))), "KMGT"[exp])
245+
}
246+
247+
// FormatSizeHuman formats bytes into a human-readable notation, e.g. 2.1 MB
248+
func FormatSizeHuman(b int64) string {
231249
const unit = 1024
232250
if b < unit {
233251
return fmt.Sprintf("%d bytes", b)
@@ -237,7 +255,7 @@ func FormatSize(b int64) string {
237255
div *= unit
238256
exp++
239257
}
240-
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
258+
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGT"[exp])
241259
}
242260

243261
// ReadPassword will read a password from STDIN. If the terminal supports it, it will not print the

util/util_test.go

+25-11
Original file line numberDiff line numberDiff line change
@@ -110,35 +110,49 @@ func TestShortTopicURL(t *testing.T) {
110110

111111
func TestParseSize_10GSuccess(t *testing.T) {
112112
s, err := ParseSize("10G")
113-
if err != nil {
114-
t.Fatal(err)
115-
}
113+
require.Nil(t, err)
116114
require.Equal(t, int64(10*1024*1024*1024), s)
117115
}
118116

119117
func TestParseSize_10MUpperCaseSuccess(t *testing.T) {
120118
s, err := ParseSize("10M")
121-
if err != nil {
122-
t.Fatal(err)
123-
}
119+
require.Nil(t, err)
124120
require.Equal(t, int64(10*1024*1024), s)
125121
}
126122

127123
func TestParseSize_10kLowerCaseSuccess(t *testing.T) {
128124
s, err := ParseSize("10k")
129-
if err != nil {
130-
t.Fatal(err)
131-
}
125+
require.Nil(t, err)
132126
require.Equal(t, int64(10*1024), s)
133127
}
134128

135129
func TestParseSize_FailureInvalid(t *testing.T) {
136130
_, err := ParseSize("not a size")
137-
if err == nil {
138-
t.Fatalf("expected error, but got none")
131+
require.Nil(t, err)
132+
}
133+
134+
func TestFormatSize(t *testing.T) {
135+
values := []struct {
136+
size int64
137+
expected string
138+
}{
139+
{10, "10"},
140+
{10 * 1024, "10K"},
141+
{10 * 1024 * 1024, "10M"},
142+
{10 * 1024 * 1024 * 1024, "10G"},
143+
}
144+
for _, value := range values {
145+
require.Equal(t, value.expected, FormatSize(value.size))
146+
s, err := ParseSize(FormatSize(value.size))
147+
require.Nil(t, err)
148+
require.Equalf(t, value.size, s, "size does not match: %d != %d", value.size, s)
139149
}
140150
}
141151

152+
func TestFormatSize_Rounded(t *testing.T) {
153+
require.Equal(t, "10K", FormatSize(10*1024+999))
154+
}
155+
142156
func TestSplitKV(t *testing.T) {
143157
key, value := SplitKV(" key = value ", "=")
144158
require.Equal(t, "key", key)

0 commit comments

Comments
 (0)