Daily traffic limit
This commit is contained in:
parent
c76e55a1c8
commit
aa94410308
7 changed files with 170 additions and 88 deletions
11
cmd/serve.go
11
cmd/serve.go
|
@ -2,11 +2,13 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"heckel.io/ntfy/server"
|
"heckel.io/ntfy/server"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,6 +38,7 @@ var flagsServe = []cli.Flag{
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "50M", Usage: "total storage limit used for attachments per visitor"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "50M", Usage: "total storage limit used for attachments per visitor"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-traffic-limit", EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_TRAFFIC_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload traffic limit per visitor"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
|
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
|
||||||
|
@ -90,6 +93,7 @@ func execServe(c *cli.Context) error {
|
||||||
totalTopicLimit := c.Int("global-topic-limit")
|
totalTopicLimit := c.Int("global-topic-limit")
|
||||||
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
|
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
|
||||||
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
|
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
|
||||||
|
visitorAttachmentDailyTrafficLimitStr := c.String("visitor-attachment-daily-traffic-limit")
|
||||||
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
|
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
|
||||||
visitorRequestLimitReplenish := c.Duration("visitor-request-limit-replenish")
|
visitorRequestLimitReplenish := c.Duration("visitor-request-limit-replenish")
|
||||||
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
|
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
|
||||||
|
@ -130,6 +134,12 @@ func execServe(c *cli.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
visitorAttachmentDailyTrafficLimit, err := parseSize(visitorAttachmentDailyTrafficLimitStr, server.DefaultVisitorAttachmentDailyTrafficLimit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if visitorAttachmentDailyTrafficLimit > math.MaxInt {
|
||||||
|
return fmt.Errorf("config option visitor-attachment-daily-traffic-limit must be lower than %d", math.MaxInt)
|
||||||
|
}
|
||||||
|
|
||||||
// Run server
|
// Run server
|
||||||
conf := server.NewConfig()
|
conf := server.NewConfig()
|
||||||
|
@ -157,6 +167,7 @@ func execServe(c *cli.Context) error {
|
||||||
conf.TotalTopicLimit = totalTopicLimit
|
conf.TotalTopicLimit = totalTopicLimit
|
||||||
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
||||||
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
|
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
|
||||||
|
conf.VisitorAttachmentDailyTrafficLimit = int(visitorAttachmentDailyTrafficLimit)
|
||||||
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
|
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
|
||||||
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
|
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
|
||||||
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
||||||
|
|
|
@ -274,7 +274,7 @@ firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
|
||||||
By default, ntfy runs without authentication, so it is vitally important that we protect the server from abuse or overload.
|
By default, ntfy runs without authentication, so it is vitally important that we protect the server from abuse or overload.
|
||||||
There are various limits and rate limits in place that you can use to configure the server. Let's do the easy ones first:
|
There are various limits and rate limits in place that you can use to configure the server. Let's do the easy ones first:
|
||||||
|
|
||||||
* `global-topic-limit` defines the total number of topics before the server rejects new topics. It defaults to 5000.
|
* `global-topic-limit` defines the total number of topics before the server rejects new topics. It defaults to 15,000.
|
||||||
* `visitor-subscription-limit` is the number of subscriptions (open connections) per visitor. This value defaults to 30.
|
* `visitor-subscription-limit` is the number of subscriptions (open connections) per visitor. This value defaults to 30.
|
||||||
|
|
||||||
A **visitor** is identified by its IP address (or the `X-Forwarded-For` header if `behind-proxy` is set). All config
|
A **visitor** is identified by its IP address (or the `X-Forwarded-For` header if `behind-proxy` is set). All config
|
||||||
|
@ -309,7 +309,7 @@ Depending on *how you run it*, here are a few limits that are relevant:
|
||||||
|
|
||||||
### For systemd services
|
### For systemd services
|
||||||
If you're running ntfy in a systemd service (e.g. for .deb/.rpm packages), the main limiting factor is the
|
If you're running ntfy in a systemd service (e.g. for .deb/.rpm packages), the main limiting factor is the
|
||||||
`LimitNOFILE` setting in the systemd unit. The default open files limit for `ntfy.service` is 10000. You can override it
|
`LimitNOFILE` setting in the systemd unit. The default open files limit for `ntfy.service` is 10,000. You can override it
|
||||||
by creating a `/etc/systemd/system/ntfy.service.d/override.conf` file. As far as I can tell, `/etc/security/limits.conf`
|
by creating a `/etc/systemd/system/ntfy.service.d/override.conf` file. As far as I can tell, `/etc/security/limits.conf`
|
||||||
is not relevant.
|
is not relevant.
|
||||||
|
|
||||||
|
@ -322,7 +322,7 @@ is not relevant.
|
||||||
|
|
||||||
### Outside of systemd
|
### Outside of systemd
|
||||||
If you're running outside systemd, you may want to adjust your `/etc/security/limits.conf` file to
|
If you're running outside systemd, you may want to adjust your `/etc/security/limits.conf` file to
|
||||||
increase the `nofile` setting. Here's an example that increases the limit to 5000. You can find out the current setting
|
increase the `nofile` setting. Here's an example that increases the limit to 5,000. You can find out the current setting
|
||||||
by running `ulimit -n`, or manually override it temporarily by running `ulimit -n 50000`.
|
by running `ulimit -n`, or manually override it temporarily by running `ulimit -n 50000`.
|
||||||
|
|
||||||
=== "/etc/security/limits.conf"
|
=== "/etc/security/limits.conf"
|
||||||
|
@ -423,12 +423,16 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||||
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
||||||
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 55s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
|
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 55s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
|
||||||
| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
|
| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
|
||||||
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 5000 | Rate limiting: Total number of topics before the server rejects new topics. |
|
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
|
||||||
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
|
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
|
||||||
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
|
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
|
||||||
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 10s | Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
|
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 10s | Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
|
||||||
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 |Initial limit of e-mails per visitor |
|
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 |Initial limit of e-mails per visitor |
|
||||||
| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled |
|
| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled |
|
||||||
|
xx daily traffic limit
|
||||||
|
xx DefaultVisitorAttachmentTotalSizeLimit
|
||||||
|
xx attachment cache dir
|
||||||
|
xx attachment
|
||||||
|
|
||||||
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
||||||
|
|
||||||
|
|
162
server/config.go
162
server/config.go
|
@ -4,7 +4,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines default config settings
|
// Defines default config settings (excluding limits, see below)
|
||||||
const (
|
const (
|
||||||
DefaultListenHTTP = ":80"
|
DefaultListenHTTP = ":80"
|
||||||
DefaultCacheDuration = 12 * time.Hour
|
DefaultCacheDuration = 12 * time.Hour
|
||||||
|
@ -13,97 +13,107 @@ const (
|
||||||
DefaultAtSenderInterval = 10 * time.Second
|
DefaultAtSenderInterval = 10 * time.Second
|
||||||
DefaultMinDelay = 10 * time.Second
|
DefaultMinDelay = 10 * time.Second
|
||||||
DefaultMaxDelay = 3 * 24 * time.Hour
|
DefaultMaxDelay = 3 * 24 * time.Hour
|
||||||
DefaultMessageLimit = 4096 // Bytes
|
|
||||||
DefaultAttachmentTotalSizeLimit = int64(1024 * 1024 * 1024) // 1 GB
|
|
||||||
DefaultAttachmentFileSizeLimit = int64(15 * 1024 * 1024) // 15 MB
|
|
||||||
DefaultAttachmentExpiryDuration = 3 * time.Hour
|
|
||||||
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery
|
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines all the limits
|
// Defines all global and per-visitor limits
|
||||||
|
// - message size limit: the max number of bytes for a message
|
||||||
// - total topic limit: max number of topics overall
|
// - total topic limit: max number of topics overall
|
||||||
|
// - various attachment limits
|
||||||
|
const (
|
||||||
|
DefaultMessageLengthLimit = 4096 // Bytes
|
||||||
|
DefaultTotalTopicLimit = 15000
|
||||||
|
DefaultAttachmentTotalSizeLimit = int64(10 * 1024 * 1024 * 1024) // 10 GB
|
||||||
|
DefaultAttachmentFileSizeLimit = int64(15 * 1024 * 1024) // 15 MB
|
||||||
|
DefaultAttachmentExpiryDuration = 3 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defines all per-visitor limits
|
||||||
// - per visitor subscription limit: max number of subscriptions (active HTTP connections) per per-visitor/IP
|
// - per visitor subscription limit: max number of subscriptions (active HTTP connections) per per-visitor/IP
|
||||||
// - per visitor request limit: max number of PUT/GET/.. requests (here: 60 requests bucket, replenished at a rate of one per 10 seconds)
|
// - per visitor request limit: max number of PUT/GET/.. requests (here: 60 requests bucket, replenished at a rate of one per 10 seconds)
|
||||||
// - per visitor email limit: max number of emails (here: 16 email bucket, replenished at a rate of one per hour)
|
// - per visitor email limit: max number of emails (here: 16 email bucket, replenished at a rate of one per hour)
|
||||||
// - per visitor attachment size limit:
|
// - per visitor attachment size limit: total per-visitor attachment size in bytes to be stored on the server
|
||||||
|
// - per visitor attachment daily traffic limit: number of bytes that can be transferred to/from the server
|
||||||
const (
|
const (
|
||||||
DefaultTotalTopicLimit = 5000
|
DefaultVisitorSubscriptionLimit = 30
|
||||||
DefaultVisitorSubscriptionLimit = 30
|
DefaultVisitorRequestLimitBurst = 60
|
||||||
DefaultVisitorRequestLimitBurst = 60
|
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
||||||
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
DefaultVisitorEmailLimitBurst = 16
|
||||||
DefaultVisitorEmailLimitBurst = 16
|
DefaultVisitorEmailLimitReplenish = time.Hour
|
||||||
DefaultVisitorEmailLimitReplenish = time.Hour
|
DefaultVisitorAttachmentTotalSizeLimit = 100 * 1024 * 1024 // 100 MB
|
||||||
DefaultVisitorAttachmentTotalSizeLimit = 50 * 1024 * 1024
|
DefaultVisitorAttachmentDailyTrafficLimit = 500 * 1024 * 1024 // 500 MB
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the main config struct for the application. Use New to instantiate a default config struct.
|
// Config is the main config struct for the application. Use New to instantiate a default config struct.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
BaseURL string
|
BaseURL string
|
||||||
ListenHTTP string
|
ListenHTTP string
|
||||||
ListenHTTPS string
|
ListenHTTPS string
|
||||||
KeyFile string
|
KeyFile string
|
||||||
CertFile string
|
CertFile string
|
||||||
FirebaseKeyFile string
|
FirebaseKeyFile string
|
||||||
CacheFile string
|
CacheFile string
|
||||||
CacheDuration time.Duration
|
CacheDuration time.Duration
|
||||||
AttachmentCacheDir string
|
AttachmentCacheDir string
|
||||||
AttachmentTotalSizeLimit int64
|
AttachmentTotalSizeLimit int64
|
||||||
AttachmentFileSizeLimit int64
|
AttachmentFileSizeLimit int64
|
||||||
AttachmentExpiryDuration time.Duration
|
AttachmentExpiryDuration time.Duration
|
||||||
KeepaliveInterval time.Duration
|
KeepaliveInterval time.Duration
|
||||||
ManagerInterval time.Duration
|
ManagerInterval time.Duration
|
||||||
AtSenderInterval time.Duration
|
AtSenderInterval time.Duration
|
||||||
FirebaseKeepaliveInterval time.Duration
|
FirebaseKeepaliveInterval time.Duration
|
||||||
SMTPSenderAddr string
|
SMTPSenderAddr string
|
||||||
SMTPSenderUser string
|
SMTPSenderUser string
|
||||||
SMTPSenderPass string
|
SMTPSenderPass string
|
||||||
SMTPSenderFrom string
|
SMTPSenderFrom string
|
||||||
SMTPServerListen string
|
SMTPServerListen string
|
||||||
SMTPServerDomain string
|
SMTPServerDomain string
|
||||||
SMTPServerAddrPrefix string
|
SMTPServerAddrPrefix string
|
||||||
MessageLimit int
|
MessageLimit int
|
||||||
MinDelay time.Duration
|
MinDelay time.Duration
|
||||||
MaxDelay time.Duration
|
MaxDelay time.Duration
|
||||||
TotalTopicLimit int
|
TotalTopicLimit int
|
||||||
TotalAttachmentSizeLimit int64
|
TotalAttachmentSizeLimit int64
|
||||||
VisitorSubscriptionLimit int
|
VisitorSubscriptionLimit int
|
||||||
VisitorAttachmentTotalSizeLimit int64
|
VisitorAttachmentTotalSizeLimit int64
|
||||||
VisitorRequestLimitBurst int
|
VisitorAttachmentDailyTrafficLimit int
|
||||||
VisitorRequestLimitReplenish time.Duration
|
VisitorRequestLimitBurst int
|
||||||
VisitorEmailLimitBurst int
|
VisitorRequestLimitReplenish time.Duration
|
||||||
VisitorEmailLimitReplenish time.Duration
|
VisitorEmailLimitBurst int
|
||||||
BehindProxy bool
|
VisitorEmailLimitReplenish time.Duration
|
||||||
|
BehindProxy bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig instantiates a default new server config
|
// NewConfig instantiates a default new server config
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
BaseURL: "",
|
BaseURL: "",
|
||||||
ListenHTTP: DefaultListenHTTP,
|
ListenHTTP: DefaultListenHTTP,
|
||||||
ListenHTTPS: "",
|
ListenHTTPS: "",
|
||||||
KeyFile: "",
|
KeyFile: "",
|
||||||
CertFile: "",
|
CertFile: "",
|
||||||
FirebaseKeyFile: "",
|
FirebaseKeyFile: "",
|
||||||
CacheFile: "",
|
CacheFile: "",
|
||||||
CacheDuration: DefaultCacheDuration,
|
CacheDuration: DefaultCacheDuration,
|
||||||
AttachmentCacheDir: "",
|
AttachmentCacheDir: "",
|
||||||
AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit,
|
AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit,
|
||||||
AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit,
|
AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit,
|
||||||
AttachmentExpiryDuration: DefaultAttachmentExpiryDuration,
|
AttachmentExpiryDuration: DefaultAttachmentExpiryDuration,
|
||||||
KeepaliveInterval: DefaultKeepaliveInterval,
|
KeepaliveInterval: DefaultKeepaliveInterval,
|
||||||
ManagerInterval: DefaultManagerInterval,
|
ManagerInterval: DefaultManagerInterval,
|
||||||
MessageLimit: DefaultMessageLimit,
|
MessageLimit: DefaultMessageLengthLimit,
|
||||||
MinDelay: DefaultMinDelay,
|
MinDelay: DefaultMinDelay,
|
||||||
MaxDelay: DefaultMaxDelay,
|
MaxDelay: DefaultMaxDelay,
|
||||||
AtSenderInterval: DefaultAtSenderInterval,
|
AtSenderInterval: DefaultAtSenderInterval,
|
||||||
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
||||||
TotalTopicLimit: DefaultTotalTopicLimit,
|
TotalTopicLimit: DefaultTotalTopicLimit,
|
||||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||||
VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit,
|
VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit,
|
||||||
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
VisitorAttachmentDailyTrafficLimit: DefaultVisitorAttachmentDailyTrafficLimit,
|
||||||
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
||||||
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
||||||
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||||
BehindProxy: false,
|
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||||
|
BehindProxy: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,12 +139,13 @@ var (
|
||||||
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
|
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
|
||||||
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
|
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
|
||||||
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", ""}
|
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", ""}
|
||||||
errHTTPBadRequestAttachmentTooLarge = &errHTTP{40012, http.StatusBadRequest, "invalid request: attachment too large", ""}
|
errHTTPBadRequestAttachmentTooLarge = &errHTTP{40012, http.StatusBadRequest, "invalid request: attachment too large, or traffic limit reached", ""}
|
||||||
errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", ""}
|
errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", ""}
|
||||||
errHTTPBadRequestAttachmentURLPeakGeneral = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachment URL peak failed", ""}
|
errHTTPBadRequestAttachmentURLPeakGeneral = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachment URL peak failed", ""}
|
||||||
errHTTPBadRequestAttachmentURLPeakNon2xx = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment URL peak failed with non-2xx status code", ""}
|
errHTTPBadRequestAttachmentURLPeakNon2xx = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment URL peak failed with non-2xx status code", ""}
|
||||||
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40016, http.StatusBadRequest, "invalid request: attachments not allowed", ""}
|
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40016, http.StatusBadRequest, "invalid request: attachments not allowed", ""}
|
||||||
errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40017, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", ""}
|
errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40017, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", ""}
|
||||||
|
errHTTPTooManyRequestsAttachmentTrafficLimit = &errHTTP{42901, http.StatusTooManyRequests, "too many requests: daily traffic limit reached", "https://ntfy.sh/docs/publish/#limitations"}
|
||||||
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", ""}
|
||||||
)
|
)
|
||||||
|
@ -341,7 +342,7 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
||||||
if e, ok = err.(*errHTTP); !ok {
|
if e, ok = err.(*errHTTP); !ok {
|
||||||
e = errHTTPInternalError
|
e = errHTTPInternalError
|
||||||
}
|
}
|
||||||
log.Printf("[%s] %s - %d - %s", r.RemoteAddr, r.Method, e.HTTPCode, err.Error())
|
log.Printf("[%s] %s - %d - %d - %s", r.RemoteAddr, r.Method, e.HTTPCode, e.Code, err.Error())
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
||||||
w.WriteHeader(e.HTTPCode)
|
w.WriteHeader(e.HTTPCode)
|
||||||
|
@ -417,7 +418,7 @@ func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if s.config.AttachmentCacheDir == "" {
|
if s.config.AttachmentCacheDir == "" {
|
||||||
return errHTTPInternalError
|
return errHTTPInternalError
|
||||||
}
|
}
|
||||||
|
@ -431,6 +432,9 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, _ *visitor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errHTTPNotFound
|
return errHTTPNotFound
|
||||||
}
|
}
|
||||||
|
if err := v.TrafficLimiter().Allow(stat.Size()); err != nil {
|
||||||
|
return errHTTPTooManyRequestsAttachmentTrafficLimit
|
||||||
|
}
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
|
||||||
f, err := os.Open(file)
|
f, err := os.Open(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -648,7 +652,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
||||||
if m.Message == "" {
|
if m.Message == "" {
|
||||||
m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)
|
m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)
|
||||||
}
|
}
|
||||||
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, util.NewFixedLimiter(remainingVisitorAttachmentSize))
|
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.TrafficLimiter(), util.NewFixedLimiter(remainingVisitorAttachmentSize))
|
||||||
if err == util.ErrLimitReached {
|
if err == util.ErrLimitReached {
|
||||||
return errHTTPBadRequestAttachmentTooLarge
|
return errHTTPBadRequestAttachmentTooLarge
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
|
|
@ -87,7 +87,7 @@
|
||||||
|
|
||||||
# Rate limiting: Total number of topics before the server rejects new topics.
|
# Rate limiting: Total number of topics before the server rejects new topics.
|
||||||
#
|
#
|
||||||
# global-topic-limit: 5000
|
# global-topic-limit: 15000
|
||||||
|
|
||||||
# Rate limiting: Number of subscriptions per visitor (IP address)
|
# Rate limiting: Number of subscriptions per visitor (IP address)
|
||||||
#
|
#
|
||||||
|
|
|
@ -826,7 +826,6 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) {
|
||||||
|
|
||||||
// Publish and make sure we can retrieve it
|
// Publish and make sure we can retrieve it
|
||||||
response := request(t, s, "PUT", "/mytopic", content, nil)
|
response := request(t, s, "PUT", "/mytopic", content, nil)
|
||||||
println(response.Body.String())
|
|
||||||
msg := toMessage(t, response.Body.String())
|
msg := toMessage(t, response.Body.String())
|
||||||
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||||
file := filepath.Join(s.config.AttachmentCacheDir, msg.ID)
|
file := filepath.Join(s.config.AttachmentCacheDir, msg.ID)
|
||||||
|
@ -845,6 +844,54 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) {
|
||||||
require.Equal(t, 404, response.Code)
|
require.Equal(t, 404, response.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishAttachmentTrafficLimit(t *testing.T) {
|
||||||
|
content := util.RandomString(5000) // > 4096
|
||||||
|
|
||||||
|
c := newTestConfig(t)
|
||||||
|
c.VisitorAttachmentDailyTrafficLimit = 5*5000 + 123 // A little more than 1 upload and 3 downloads
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
|
// Publish attachment
|
||||||
|
response := request(t, s, "PUT", "/mytopic", content, nil)
|
||||||
|
msg := toMessage(t, response.Body.String())
|
||||||
|
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||||
|
|
||||||
|
// Get it 4 times successfully
|
||||||
|
path := strings.TrimPrefix(msg.Attachment.URL, "http://127.0.0.1:12345")
|
||||||
|
for i := 1; i <= 4; i++ { // 4 successful downloads
|
||||||
|
response = request(t, s, "GET", path, "", nil)
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
require.Equal(t, content, response.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// And then fail with a 429
|
||||||
|
response = request(t, s, "GET", path, "", nil)
|
||||||
|
err := toHTTPError(t, response.Body.String())
|
||||||
|
require.Equal(t, 429, response.Code)
|
||||||
|
require.Equal(t, 42901, err.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishAttachmentTrafficLimitUploadOnly(t *testing.T) {
|
||||||
|
content := util.RandomString(5000) // > 4096
|
||||||
|
|
||||||
|
c := newTestConfig(t)
|
||||||
|
c.VisitorAttachmentDailyTrafficLimit = 5*5000 + 500 // 5 successful uploads
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
|
||||||
|
// 5 successful uploads
|
||||||
|
for i := 1; i <= 5; i++ {
|
||||||
|
response := request(t, s, "PUT", "/mytopic", content, nil)
|
||||||
|
msg := toMessage(t, response.Body.String())
|
||||||
|
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// And a failed one
|
||||||
|
response := request(t, s, "PUT", "/mytopic", content, nil)
|
||||||
|
err := toHTTPError(t, response.Body.String())
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, 40012, err.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func newTestConfig(t *testing.T) *Config {
|
func newTestConfig(t *testing.T) *Config {
|
||||||
conf := NewConfig()
|
conf := NewConfig()
|
||||||
conf.BaseURL = "http://127.0.0.1:12345"
|
conf.BaseURL = "http://127.0.0.1:12345"
|
||||||
|
|
|
@ -24,8 +24,9 @@ type visitor struct {
|
||||||
config *Config
|
config *Config
|
||||||
ip string
|
ip string
|
||||||
requests *rate.Limiter
|
requests *rate.Limiter
|
||||||
subscriptions util.Limiter
|
|
||||||
emails *rate.Limiter
|
emails *rate.Limiter
|
||||||
|
subscriptions util.Limiter
|
||||||
|
traffic util.Limiter
|
||||||
seen time.Time
|
seen time.Time
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
@ -35,8 +36,9 @@ func newVisitor(conf *Config, ip string) *visitor {
|
||||||
config: conf,
|
config: conf,
|
||||||
ip: ip,
|
ip: ip,
|
||||||
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
||||||
subscriptions: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)),
|
|
||||||
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
|
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
|
||||||
|
subscriptions: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)),
|
||||||
|
traffic: util.NewBytesLimiter(conf.VisitorAttachmentDailyTrafficLimit, 24*time.Hour),
|
||||||
seen: time.Now(),
|
seen: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,6 +82,10 @@ func (v *visitor) Keepalive() {
|
||||||
v.seen = time.Now()
|
v.seen = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *visitor) TrafficLimiter() util.Limiter {
|
||||||
|
return v.traffic
|
||||||
|
}
|
||||||
|
|
||||||
func (v *visitor) Stale() bool {
|
func (v *visitor) Stale() bool {
|
||||||
v.mu.Lock()
|
v.mu.Lock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.Unlock()
|
||||||
|
|
Loading…
Reference in a new issue