mirror of
https://github.com/binwiederhier/ntfy.git
synced 2025-06-10 15:04:20 +02:00
Daily traffic limit
This commit is contained in:
parent
c76e55a1c8
commit
aa94410308
7 changed files with 170 additions and 88 deletions
162
server/config.go
162
server/config.go
|
@ -4,7 +4,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// Defines default config settings
|
||||
// Defines default config settings (excluding limits, see below)
|
||||
const (
|
||||
DefaultListenHTTP = ":80"
|
||||
DefaultCacheDuration = 12 * time.Hour
|
||||
|
@ -13,97 +13,107 @@ const (
|
|||
DefaultAtSenderInterval = 10 * time.Second
|
||||
DefaultMinDelay = 10 * time.Second
|
||||
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
|
||||
)
|
||||
|
||||
// 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
|
||||
// - 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 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 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 (
|
||||
DefaultTotalTopicLimit = 5000
|
||||
DefaultVisitorSubscriptionLimit = 30
|
||||
DefaultVisitorRequestLimitBurst = 60
|
||||
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
||||
DefaultVisitorEmailLimitBurst = 16
|
||||
DefaultVisitorEmailLimitReplenish = time.Hour
|
||||
DefaultVisitorAttachmentTotalSizeLimit = 50 * 1024 * 1024
|
||||
DefaultVisitorSubscriptionLimit = 30
|
||||
DefaultVisitorRequestLimitBurst = 60
|
||||
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
||||
DefaultVisitorEmailLimitBurst = 16
|
||||
DefaultVisitorEmailLimitReplenish = time.Hour
|
||||
DefaultVisitorAttachmentTotalSizeLimit = 100 * 1024 * 1024 // 100 MB
|
||||
DefaultVisitorAttachmentDailyTrafficLimit = 500 * 1024 * 1024 // 500 MB
|
||||
)
|
||||
|
||||
// Config is the main config struct for the application. Use New to instantiate a default config struct.
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
ListenHTTP string
|
||||
ListenHTTPS string
|
||||
KeyFile string
|
||||
CertFile string
|
||||
FirebaseKeyFile string
|
||||
CacheFile string
|
||||
CacheDuration time.Duration
|
||||
AttachmentCacheDir string
|
||||
AttachmentTotalSizeLimit int64
|
||||
AttachmentFileSizeLimit int64
|
||||
AttachmentExpiryDuration time.Duration
|
||||
KeepaliveInterval time.Duration
|
||||
ManagerInterval time.Duration
|
||||
AtSenderInterval time.Duration
|
||||
FirebaseKeepaliveInterval time.Duration
|
||||
SMTPSenderAddr string
|
||||
SMTPSenderUser string
|
||||
SMTPSenderPass string
|
||||
SMTPSenderFrom string
|
||||
SMTPServerListen string
|
||||
SMTPServerDomain string
|
||||
SMTPServerAddrPrefix string
|
||||
MessageLimit int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
TotalTopicLimit int
|
||||
TotalAttachmentSizeLimit int64
|
||||
VisitorSubscriptionLimit int
|
||||
VisitorAttachmentTotalSizeLimit int64
|
||||
VisitorRequestLimitBurst int
|
||||
VisitorRequestLimitReplenish time.Duration
|
||||
VisitorEmailLimitBurst int
|
||||
VisitorEmailLimitReplenish time.Duration
|
||||
BehindProxy bool
|
||||
BaseURL string
|
||||
ListenHTTP string
|
||||
ListenHTTPS string
|
||||
KeyFile string
|
||||
CertFile string
|
||||
FirebaseKeyFile string
|
||||
CacheFile string
|
||||
CacheDuration time.Duration
|
||||
AttachmentCacheDir string
|
||||
AttachmentTotalSizeLimit int64
|
||||
AttachmentFileSizeLimit int64
|
||||
AttachmentExpiryDuration time.Duration
|
||||
KeepaliveInterval time.Duration
|
||||
ManagerInterval time.Duration
|
||||
AtSenderInterval time.Duration
|
||||
FirebaseKeepaliveInterval time.Duration
|
||||
SMTPSenderAddr string
|
||||
SMTPSenderUser string
|
||||
SMTPSenderPass string
|
||||
SMTPSenderFrom string
|
||||
SMTPServerListen string
|
||||
SMTPServerDomain string
|
||||
SMTPServerAddrPrefix string
|
||||
MessageLimit int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
TotalTopicLimit int
|
||||
TotalAttachmentSizeLimit int64
|
||||
VisitorSubscriptionLimit int
|
||||
VisitorAttachmentTotalSizeLimit int64
|
||||
VisitorAttachmentDailyTrafficLimit int
|
||||
VisitorRequestLimitBurst int
|
||||
VisitorRequestLimitReplenish time.Duration
|
||||
VisitorEmailLimitBurst int
|
||||
VisitorEmailLimitReplenish time.Duration
|
||||
BehindProxy bool
|
||||
}
|
||||
|
||||
// NewConfig instantiates a default new server config
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
BaseURL: "",
|
||||
ListenHTTP: DefaultListenHTTP,
|
||||
ListenHTTPS: "",
|
||||
KeyFile: "",
|
||||
CertFile: "",
|
||||
FirebaseKeyFile: "",
|
||||
CacheFile: "",
|
||||
CacheDuration: DefaultCacheDuration,
|
||||
AttachmentCacheDir: "",
|
||||
AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit,
|
||||
AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit,
|
||||
AttachmentExpiryDuration: DefaultAttachmentExpiryDuration,
|
||||
KeepaliveInterval: DefaultKeepaliveInterval,
|
||||
ManagerInterval: DefaultManagerInterval,
|
||||
MessageLimit: DefaultMessageLimit,
|
||||
MinDelay: DefaultMinDelay,
|
||||
MaxDelay: DefaultMaxDelay,
|
||||
AtSenderInterval: DefaultAtSenderInterval,
|
||||
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
||||
TotalTopicLimit: DefaultTotalTopicLimit,
|
||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||
VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit,
|
||||
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
||||
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
||||
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||
BehindProxy: false,
|
||||
BaseURL: "",
|
||||
ListenHTTP: DefaultListenHTTP,
|
||||
ListenHTTPS: "",
|
||||
KeyFile: "",
|
||||
CertFile: "",
|
||||
FirebaseKeyFile: "",
|
||||
CacheFile: "",
|
||||
CacheDuration: DefaultCacheDuration,
|
||||
AttachmentCacheDir: "",
|
||||
AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit,
|
||||
AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit,
|
||||
AttachmentExpiryDuration: DefaultAttachmentExpiryDuration,
|
||||
KeepaliveInterval: DefaultKeepaliveInterval,
|
||||
ManagerInterval: DefaultManagerInterval,
|
||||
MessageLimit: DefaultMessageLengthLimit,
|
||||
MinDelay: DefaultMinDelay,
|
||||
MaxDelay: DefaultMaxDelay,
|
||||
AtSenderInterval: DefaultAtSenderInterval,
|
||||
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
||||
TotalTopicLimit: DefaultTotalTopicLimit,
|
||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||
VisitorAttachmentTotalSizeLimit: DefaultVisitorAttachmentTotalSizeLimit,
|
||||
VisitorAttachmentDailyTrafficLimit: DefaultVisitorAttachmentDailyTrafficLimit,
|
||||
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
||||
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
||||
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||
BehindProxy: false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,12 +139,13 @@ var (
|
|||
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
|
||||
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
|
||||
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", ""}
|
||||
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", ""}
|
||||
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40016, http.StatusBadRequest, "invalid request: attachments not allowed", ""}
|
||||
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", ""}
|
||||
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 {
|
||||
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("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
|
||||
w.WriteHeader(e.HTTPCode)
|
||||
|
@ -417,7 +418,7 @@ func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
|
|||
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 == "" {
|
||||
return errHTTPInternalError
|
||||
}
|
||||
|
@ -431,6 +432,9 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, _ *visitor)
|
|||
if err != nil {
|
||||
return errHTTPNotFound
|
||||
}
|
||||
if err := v.TrafficLimiter().Allow(stat.Size()); err != nil {
|
||||
return errHTTPTooManyRequestsAttachmentTrafficLimit
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
|
@ -648,7 +652,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, util.NewFixedLimiter(remainingVisitorAttachmentSize))
|
||||
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, v.TrafficLimiter(), util.NewFixedLimiter(remainingVisitorAttachmentSize))
|
||||
if err == util.ErrLimitReached {
|
||||
return errHTTPBadRequestAttachmentTooLarge
|
||||
} else if err != nil {
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
|
||||
# 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)
|
||||
#
|
||||
|
|
|
@ -826,7 +826,6 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) {
|
|||
|
||||
// Publish and make sure we can retrieve it
|
||||
response := request(t, s, "PUT", "/mytopic", content, nil)
|
||||
println(response.Body.String())
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.Contains(t, msg.Attachment.URL, "http://127.0.0.1:12345/file/")
|
||||
file := filepath.Join(s.config.AttachmentCacheDir, msg.ID)
|
||||
|
@ -845,6 +844,54 @@ func TestServer_PublishAttachmentAndPrune(t *testing.T) {
|
|||
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 {
|
||||
conf := NewConfig()
|
||||
conf.BaseURL = "http://127.0.0.1:12345"
|
||||
|
|
|
@ -24,8 +24,9 @@ type visitor struct {
|
|||
config *Config
|
||||
ip string
|
||||
requests *rate.Limiter
|
||||
subscriptions util.Limiter
|
||||
emails *rate.Limiter
|
||||
subscriptions util.Limiter
|
||||
traffic util.Limiter
|
||||
seen time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
@ -35,8 +36,9 @@ func newVisitor(conf *Config, ip string) *visitor {
|
|||
config: conf,
|
||||
ip: ip,
|
||||
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
||||
subscriptions: util.NewFixedLimiter(int64(conf.VisitorSubscriptionLimit)),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +82,10 @@ func (v *visitor) Keepalive() {
|
|||
v.seen = time.Now()
|
||||
}
|
||||
|
||||
func (v *visitor) TrafficLimiter() util.Limiter {
|
||||
return v.traffic
|
||||
}
|
||||
|
||||
func (v *visitor) Stale() bool {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue