Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020c058805 | ||
|
|
8a625ef786 | ||
|
|
3bc8ff0104 | ||
|
|
11b5ac49c0 | ||
|
|
f553cdb282 | ||
|
|
6b46eb46e2 | ||
|
|
7280ae1ebc | ||
|
|
873c57b3d8 |
@@ -37,6 +37,8 @@ func TestClient_Publish_Subscribe(t *testing.T) {
|
|||||||
require.Equal(t, "some delayed message", msg.Message)
|
require.Equal(t, "some delayed message", msg.Message)
|
||||||
require.True(t, time.Now().Add(24*time.Hour).Unix() < msg.Time)
|
require.True(t, time.Now().Add(24*time.Hour).Unix() < msg.Time)
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
msg = nextMessage(c)
|
msg = nextMessage(c)
|
||||||
require.NotNil(t, msg)
|
require.NotNil(t, msg)
|
||||||
require.Equal(t, "some message", msg.Message)
|
require.Equal(t, "some message", msg.Message)
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ func WithDelay(delay string) PublishOption {
|
|||||||
return WithHeader("X-Delay", delay)
|
return WithHeader("X-Delay", delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithEmail instructs the server to also send the message to the given e-mail address
|
||||||
|
func WithEmail(email string) PublishOption {
|
||||||
|
return WithHeader("X-Email", email)
|
||||||
|
}
|
||||||
|
|
||||||
// WithNoCache instructs the server not to cache the message server-side
|
// WithNoCache instructs the server not to cache the message server-side
|
||||||
func WithNoCache() PublishOption {
|
func WithNoCache() PublishOption {
|
||||||
return WithHeader("X-Cache", "no")
|
return WithHeader("X-Cache", "no")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/urfave/cli/v2/altsrc"
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -60,7 +59,3 @@ func initConfigFileInputSource(configFlag string, flags []cli.Flag) cli.BeforeFu
|
|||||||
return altsrc.ApplyInputSourceValues(context, inputSource, flags)
|
return altsrc.ApplyInputSourceValues(context, inputSource, flags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collapseTopicURL(s string) string {
|
|
||||||
return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/client"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,3 +27,11 @@ func newTestApp() (*cli.App, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
|
|||||||
app.ErrWriter = &stderr
|
app.ErrWriter = &stderr
|
||||||
return app, &stdin, &stdout, &stderr
|
return app, &stdin, &stdout, &stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toMessage(t *testing.T, s string) *client.Message {
|
||||||
|
var m *client.Message
|
||||||
|
if err := json.NewDecoder(strings.NewReader(s)).Decode(&m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ var cmdPublish = &cli.Command{
|
|||||||
&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
|
&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
|
||||||
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"},
|
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"},
|
||||||
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"},
|
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"},
|
||||||
|
&cli.StringFlag{Name: "email", Aliases: []string{"e-mail", "mail", "e"}, Usage: "also send to e-mail address"},
|
||||||
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"},
|
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"},
|
||||||
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"},
|
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"},
|
||||||
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Usage: "do print message"},
|
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Usage: "do print message"},
|
||||||
@@ -33,6 +34,7 @@ Examples:
|
|||||||
ntfy pub --tags=warning,skull backups "Backups failed" # Add tags/emojis to message
|
ntfy pub --tags=warning,skull backups "Backups failed" # Add tags/emojis to message
|
||||||
ntfy pub --delay=10s delayed_topic Laterzz # Delay message by 10s
|
ntfy pub --delay=10s delayed_topic Laterzz # Delay message by 10s
|
||||||
ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am
|
ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am
|
||||||
|
ntfy pub -e phil@example.com alerts 'App is down!' # Also send email to phil@example.com
|
||||||
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
||||||
|
|
||||||
Please also check out the docs on publishing messages. Especially for the --tags and --delay options,
|
Please also check out the docs on publishing messages. Especially for the --tags and --delay options,
|
||||||
@@ -54,6 +56,7 @@ func execPublish(c *cli.Context) error {
|
|||||||
priority := c.String("priority")
|
priority := c.String("priority")
|
||||||
tags := c.String("tags")
|
tags := c.String("tags")
|
||||||
delay := c.String("delay")
|
delay := c.String("delay")
|
||||||
|
email := c.String("email")
|
||||||
noCache := c.Bool("no-cache")
|
noCache := c.Bool("no-cache")
|
||||||
noFirebase := c.Bool("no-firebase")
|
noFirebase := c.Bool("no-firebase")
|
||||||
quiet := c.Bool("quiet")
|
quiet := c.Bool("quiet")
|
||||||
@@ -75,6 +78,9 @@ func execPublish(c *cli.Context) error {
|
|||||||
if delay != "" {
|
if delay != "" {
|
||||||
options = append(options, client.WithDelay(delay))
|
options = append(options, client.WithDelay(delay))
|
||||||
}
|
}
|
||||||
|
if email != "" {
|
||||||
|
options = append(options, client.WithEmail(email))
|
||||||
|
}
|
||||||
if noCache {
|
if noCache {
|
||||||
options = append(options, client.WithNoCache())
|
options = append(options, client.WithNoCache())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -16,3 +18,19 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
|||||||
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
||||||
require.Contains(t, stdout.String(), testMessage)
|
require.Contains(t, stdout.String(), testMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
||||||
|
s, port := test.StartServer(t)
|
||||||
|
defer test.StopServer(t, s, port)
|
||||||
|
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", topic, "some message"}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "some message", m.Message)
|
||||||
|
|
||||||
|
app2, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", topic}))
|
||||||
|
m = toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "some message", m.Message)
|
||||||
|
}
|
||||||
|
|||||||
30
cmd/serve.go
30
cmd/serve.go
@@ -1,4 +1,3 @@
|
|||||||
// Package cmd provides the ntfy CLI application
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -13,6 +12,7 @@ import (
|
|||||||
|
|
||||||
var flagsServe = []cli.Flag{
|
var flagsServe = []cli.Flag{
|
||||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
|
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}),
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
|
||||||
@@ -22,10 +22,16 @@ var flagsServe = []cli.Flag{
|
|||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
|
||||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
|
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-addr", EnvVars: []string{"NTFY_SMTP_ADDR"}, Usage: "SMTP server address (host:port) to allow email sending"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-user", EnvVars: []string{"NTFY_SMTP_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-pass", EnvVars: []string{"NTFY_SMTP_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
|
||||||
|
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-from", EnvVars: []string{"NTFY_SMTP_FROM"}, Usage: "SMTP sender address (if e-mail sending is enabled)"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultGlobalTopicLimit, 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.DefaultGlobalTopicLimit, Usage: "total number of topics allowed"}),
|
||||||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"V"}, 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.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", Aliases: []string{"B"}, 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", Aliases: []string{"R"}, 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.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
|
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +58,7 @@ func execServe(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read all the options
|
// Read all the options
|
||||||
|
baseURL := c.String("base-url")
|
||||||
listenHTTP := c.String("listen-http")
|
listenHTTP := c.String("listen-http")
|
||||||
listenHTTPS := c.String("listen-https")
|
listenHTTPS := c.String("listen-https")
|
||||||
keyFile := c.String("key-file")
|
keyFile := c.String("key-file")
|
||||||
@@ -61,10 +68,16 @@ func execServe(c *cli.Context) error {
|
|||||||
cacheDuration := c.Duration("cache-duration")
|
cacheDuration := c.Duration("cache-duration")
|
||||||
keepaliveInterval := c.Duration("keepalive-interval")
|
keepaliveInterval := c.Duration("keepalive-interval")
|
||||||
managerInterval := c.Duration("manager-interval")
|
managerInterval := c.Duration("manager-interval")
|
||||||
|
smtpAddr := c.String("smtp-addr")
|
||||||
|
smtpUser := c.String("smtp-user")
|
||||||
|
smtpPass := c.String("smtp-pass")
|
||||||
|
smtpFrom := c.String("smtp-from")
|
||||||
globalTopicLimit := c.Int("global-topic-limit")
|
globalTopicLimit := c.Int("global-topic-limit")
|
||||||
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
|
visitorSubscriptionLimit := c.Int("visitor-subscription-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")
|
||||||
|
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
|
||||||
behindProxy := c.Bool("behind-proxy")
|
behindProxy := c.Bool("behind-proxy")
|
||||||
|
|
||||||
// Check values
|
// Check values
|
||||||
@@ -82,10 +95,13 @@ func execServe(c *cli.Context) error {
|
|||||||
return errors.New("if set, certificate file must exist")
|
return errors.New("if set, certificate file must exist")
|
||||||
} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
|
} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
|
||||||
return errors.New("if listen-https is set, both key-file and cert-file must be set")
|
return errors.New("if listen-https is set, both key-file and cert-file must be set")
|
||||||
|
} else if smtpAddr != "" && (baseURL == "" || smtpUser == "" || smtpPass == "" || smtpFrom == "") {
|
||||||
|
return errors.New("if smtp-addr is set, base-url, smtp-user, smtp-pass and smtp-from must also be set")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run server
|
// Run server
|
||||||
conf := server.NewConfig()
|
conf := server.NewConfig()
|
||||||
|
conf.BaseURL = baseURL
|
||||||
conf.ListenHTTP = listenHTTP
|
conf.ListenHTTP = listenHTTP
|
||||||
conf.ListenHTTPS = listenHTTPS
|
conf.ListenHTTPS = listenHTTPS
|
||||||
conf.KeyFile = keyFile
|
conf.KeyFile = keyFile
|
||||||
@@ -95,10 +111,16 @@ func execServe(c *cli.Context) error {
|
|||||||
conf.CacheDuration = cacheDuration
|
conf.CacheDuration = cacheDuration
|
||||||
conf.KeepaliveInterval = keepaliveInterval
|
conf.KeepaliveInterval = keepaliveInterval
|
||||||
conf.ManagerInterval = managerInterval
|
conf.ManagerInterval = managerInterval
|
||||||
|
conf.SMTPAddr = smtpAddr
|
||||||
|
conf.SMTPUser = smtpUser
|
||||||
|
conf.SMTPPass = smtpPass
|
||||||
|
conf.SMTPFrom = smtpFrom
|
||||||
conf.GlobalTopicLimit = globalTopicLimit
|
conf.GlobalTopicLimit = globalTopicLimit
|
||||||
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
||||||
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
|
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
|
||||||
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
|
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
|
||||||
|
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
||||||
|
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
||||||
conf.BehindProxy = behindProxy
|
conf.BehindProxy = behindProxy
|
||||||
s, err := server.New(conf)
|
s, err := server.New(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ func runCommandInternal(c *cli.Context, command string, m *client.Message) error
|
|||||||
defer os.Remove(scriptFile)
|
defer os.Remove(scriptFile)
|
||||||
verbose := c.Bool("verbose")
|
verbose := c.Bool("verbose")
|
||||||
if verbose {
|
if verbose {
|
||||||
log.Printf("[%s] Executing: %s (for message: %s)", collapseTopicURL(m.TopicURL), command, m.Raw)
|
log.Printf("[%s] Executing: %s (for message: %s)", util.ShortTopicURL(m.TopicURL), command, m.Raw)
|
||||||
}
|
}
|
||||||
cmd := exec.Command("sh", "-c", scriptFile)
|
cmd := exec.Command("sh", "-c", scriptFile)
|
||||||
cmd.Stdin = c.App.Reader
|
cmd.Stdin = c.App.Reader
|
||||||
|
|||||||
103
docs/config.md
103
docs/config.md
@@ -35,19 +35,34 @@ the message to the subscribers.
|
|||||||
Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscribe/api.md#poll-for-messages), as well as the
|
Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscribe/api.md#poll-for-messages), as well as the
|
||||||
[`since=` parameter](subscribe/api.md#fetch-cached-messages).
|
[`since=` parameter](subscribe/api.md#fetch-cached-messages).
|
||||||
|
|
||||||
|
## E-mail notifications
|
||||||
|
To allow forwarding messages via e-mail, you can configure an SMTP server for outgoing messages. Once configured,
|
||||||
|
you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail-notifications) (e.g.
|
||||||
|
`curl -d "hi there" -H "X-Email: phil@example.com" ntfy.sh/mytopic`).
|
||||||
|
|
||||||
|
As of today, only SMTP servers with PLAIN auth and STARTLS are supported. To enable e-mail sending, you must set the
|
||||||
|
following settings:
|
||||||
|
|
||||||
|
* `base-url` is the root URL for the ntfy server; this is needed for e-mail footer
|
||||||
|
* `smtp-addr` is the hostname:port of the SMTP server
|
||||||
|
* `smtp-user` and `smtp-pass` are the username and password of the SMTP user
|
||||||
|
* `smtp-from` is the e-mail address of the sender
|
||||||
|
|
||||||
|
Please also refer to the [rate limiting](#rate-limiting) settings below, specifically `visitor-email-limit-burst`
|
||||||
|
and `visitor-email-limit-burst`. Setting these conservatively is necessary to avoid abuse.
|
||||||
|
|
||||||
## Behind a proxy (TLS, etc.)
|
## Behind a proxy (TLS, etc.)
|
||||||
!!! warning
|
!!! warning
|
||||||
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
|
If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
|
||||||
[rate limited](#rate-limiting) as if they are one.
|
[rate limited](#rate-limiting) as if they are one.
|
||||||
|
|
||||||
It may be desirable to run ntfy behind a proxy, e.g. so you can provide TLS certificates using Let's Encrypt using certbot,
|
It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache), so you can provide TLS certificates
|
||||||
or simply because you'd like to share the ports (80/443) with other services. Whatever your reasons may be, there are a
|
using Let's Encrypt using certbot, or simply because you'd like to share the ports (80/443) with other services.
|
||||||
few things to consider.
|
Whatever your reasons may be, there are a few things to consider.
|
||||||
|
|
||||||
### Rate limiting
|
If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. This will instruct the
|
||||||
If you are running ntfy behind a proxy (e.g. nginx, HAproxy or Apache), you should set the `behind-proxy`
|
[rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary identifier for a visitor,
|
||||||
flag. This will instruct the [rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary
|
as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will
|
||||||
identifier for a visitor, as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will
|
|
||||||
be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address.
|
be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address.
|
||||||
|
|
||||||
=== "/etc/ntfy/server.yml"
|
=== "/etc/ntfy/server.yml"
|
||||||
@@ -214,7 +229,7 @@ firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
|
|||||||
## Rate limiting
|
## Rate limiting
|
||||||
!!! info
|
!!! info
|
||||||
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
|
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
|
||||||
Otherwise all visitors are rate limited as if they are one.
|
Otherwise, all visitors are rate limited as if they are one.
|
||||||
|
|
||||||
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:
|
||||||
@@ -235,9 +250,14 @@ request every 10s (defined by `visitor-request-limit-replenish`)
|
|||||||
* `visitor-request-limit-burst` is the initial bucket of requests each visitor has. This defaults to 60.
|
* `visitor-request-limit-burst` is the initial bucket of requests each visitor has. This defaults to 60.
|
||||||
* `visitor-request-limit-replenish` is the rate at which the bucket is refilled (one request per x). Defaults to 10s.
|
* `visitor-request-limit-replenish` is the rate at which the bucket is refilled (one request per x). Defaults to 10s.
|
||||||
|
|
||||||
During normal usage, you shouldn't encounter this limit at all, and even if you burst a few requests shortly (e.g. when you
|
Similarly to the request limit, there is also an e-mail limit (only relevant if [e-mail notifications](#e-mail-notifications)
|
||||||
reconnect after a connection drop), it shouldn't have any effect.
|
are enabled):
|
||||||
|
|
||||||
|
* `visitor-email-limit-burst` is the initial bucket of emails each visitor has. This defaults to 16.
|
||||||
|
* `visitor-email-limit-replenish` is the rate at which the bucket is refilled (one email per x). Defaults to 1h.
|
||||||
|
|
||||||
|
During normal usage, you shouldn't encounter these limits at all, and even if you burst a few requests or emails
|
||||||
|
(e.g. when you reconnect after a connection drop), it shouldn't have any effect.
|
||||||
|
|
||||||
## Tuning for scale
|
## Tuning for scale
|
||||||
If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
|
If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
|
||||||
@@ -300,6 +320,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||||||
|
|
||||||
| Config option | Env variable | Format | Default | Description |
|
| Config option | Env variable | Format | Default | Description |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
|
| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) |
|
||||||
| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
|
| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
|
||||||
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
|
| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. |
|
||||||
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
|
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
|
||||||
@@ -307,42 +328,64 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||||||
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). |
|
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). |
|
||||||
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
|
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
|
||||||
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
|
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
|
||||||
|
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
|
||||||
|
| `smtp-addr` | `NTFY_SMTP_ADDR` | `host:port` | - | SMTP server address to allow email sending |
|
||||||
|
| `smtp-user` | `NTFY_SMTP_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled |
|
||||||
|
| `smtp-pass` | `NTFY_SMTP_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled |
|
||||||
|
| `smtp-from` | `NTFY_SMTP_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled |
|
||||||
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 30s | 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* | 30s | 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* | 5000 | 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 |
|
||||||
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
|
| `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 |
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
## Command line options
|
## Command line options
|
||||||
```
|
```
|
||||||
$ ntfy --help
|
$ ntfy serve --help
|
||||||
NAME:
|
NAME:
|
||||||
ntfy - Simple pub-sub notification service
|
ntfy serve - Run the ntfy server
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
ntfy [OPTION..]
|
ntfy serve [OPTIONS..]
|
||||||
|
|
||||||
GLOBAL OPTIONS:
|
DESCRIPTION:
|
||||||
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
|
Run the ntfy server and listen for incoming requests
|
||||||
--listen-http value, -l value ip:port used to as listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
|
||||||
--firebase-key-file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
|
|
||||||
--cache-file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
|
|
||||||
--cache-duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
|
|
||||||
--keepalive-interval value, -k value interval of keepalive messages (default: 30s) [$NTFY_KEEPALIVE_INTERVAL]
|
|
||||||
--manager-interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
|
|
||||||
--global-topic-limit value, -T value total number of topics allowed (default: 5000) [$NTFY_GLOBAL_TOPIC_LIMIT]
|
|
||||||
--visitor-subscription-limit value, -V value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
|
|
||||||
--visitor-request-limit-burst value, -B value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
|
|
||||||
--visitor-request-limit-replenish value, -R value interval at which burst limit is replenished (one per x) (default: 10s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
|
|
||||||
--behind-proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
|
|
||||||
|
|
||||||
Try 'ntfy COMMAND --help' for more information.
|
The command will load the configuration from /etc/ntfy/server.yml. Config options can
|
||||||
|
be overridden using the command line options.
|
||||||
|
|
||||||
ntfy v1.4.8 (7b8185c), runtime go1.17, built at 1637872539
|
Examples:
|
||||||
Copyright (C) 2021 Philipp C. Heckel, distributed under the Apache License 2.0
|
ntfy serve # Starts server in the foreground (on port 80)
|
||||||
|
ntfy serve --listen-http :8080 # Starts server with alternate port
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
|
||||||
|
--base-url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
|
||||||
|
--listen-http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
|
||||||
|
--listen-https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
|
||||||
|
--key-file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
|
||||||
|
--cert-file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
|
||||||
|
--firebase-key-file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
|
||||||
|
--cache-file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
|
||||||
|
--cache-duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
|
||||||
|
--keepalive-interval value, -k value interval of keepalive messages (default: 30s) [$NTFY_KEEPALIVE_INTERVAL]
|
||||||
|
--manager-interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
|
||||||
|
--smtp-addr value SMTP server address (host:port) to allow email sending [$NTFY_SMTP_ADDR]
|
||||||
|
--smtp-user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_USER]
|
||||||
|
--smtp-pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_PASS]
|
||||||
|
--smtp-from value SMTP sender address (if e-mail sending is enabled) [$NTFY_SMTP_FROM]
|
||||||
|
--global-topic-limit value, -T value total number of topics allowed (default: 5000) [$NTFY_GLOBAL_TOPIC_LIMIT]
|
||||||
|
--visitor-subscription-limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
|
||||||
|
--visitor-request-limit-burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
|
||||||
|
--visitor-request-limit-replenish value interval at which burst limit is replenished (one per x) (default: 10s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
|
||||||
|
--visitor-email-limit-burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
|
||||||
|
--visitor-email-limit-replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
|
||||||
|
--behind-proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
|
||||||
|
--help, -h show help (default: false)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -26,21 +26,21 @@ deb/rpm packages.
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_x86_64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_x86_64.tar.gz
|
||||||
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
||||||
sudo ./ntfy serve
|
sudo ./ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_armv7.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_armv7.tar.gz
|
||||||
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
||||||
sudo ./ntfy serve
|
sudo ./ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_arm64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_arm64.tar.gz
|
||||||
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
|
||||||
sudo ./ntfy serve
|
sudo ./ntfy serve
|
||||||
```
|
```
|
||||||
@@ -88,7 +88,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_amd64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_amd64.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -96,7 +96,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_armv7.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_armv7.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -104,7 +104,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_arm64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_arm64.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -114,21 +114,21 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_amd64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_amd64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_armv7.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_armv7.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.8.0/ntfy_1.8.0_linux_arm64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.9.0/ntfy_1.9.0_linux_arm64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|||||||
103
docs/publish.md
103
docs/publish.md
@@ -592,6 +592,105 @@ Here's an example with a custom message, tags and a priority:
|
|||||||
file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull');
|
file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## E-mail notifications
|
||||||
|
You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that
|
||||||
|
you'd like to persist longer, or to blast-notify yourself on all possible channels.
|
||||||
|
|
||||||
|
Usage is easy: Simply pass the `X-Email` header (or any of its aliases: `X-E-mail`, `Email`, `E-mail`, `Mail`, or `e`).
|
||||||
|
Only one e-mail address is supported.
|
||||||
|
|
||||||
|
Since ntfy does not provide auth (yet), the rate limiting is pretty strict (see [limitations](#limitations)). In the
|
||||||
|
default configuration, you get **16 e-mails per visitor** (IP address) and then after that one per hour. On top of
|
||||||
|
that, your IP address appears in the e-mail body. This is to prevent abuse.
|
||||||
|
|
||||||
|
=== "Command line (curl)"
|
||||||
|
```
|
||||||
|
curl \
|
||||||
|
-H "Email: phil@example.com" \
|
||||||
|
-H "Tags: warning,skull,backup-host,ssh-login" \
|
||||||
|
-H "Priority: high" \
|
||||||
|
-d "Unknown login from 5.31.23.83 to backups.example.com" \
|
||||||
|
ntfy.sh/alerts
|
||||||
|
curl -H "Email: phil@example.com" -d "You've Got Mail"
|
||||||
|
curl -d "You've Got Mail" "ntfy.sh/alerts?email=phil@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "ntfy CLI"
|
||||||
|
```
|
||||||
|
ntfy publish \
|
||||||
|
--email=phil@example.com \
|
||||||
|
--tags=warning,skull,backup-host,ssh-login \
|
||||||
|
--priority=high \
|
||||||
|
alerts "Unknown login from 5.31.23.83 to backups.example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "HTTP"
|
||||||
|
``` http
|
||||||
|
POST /alerts HTTP/1.1
|
||||||
|
Host: ntfy.sh
|
||||||
|
Email: phil@example.com
|
||||||
|
Tags: warning,skull,backup-host,ssh-login
|
||||||
|
Priority: high
|
||||||
|
|
||||||
|
Unknown login from 5.31.23.83 to backups.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "JavaScript"
|
||||||
|
``` javascript
|
||||||
|
fetch('https://ntfy.sh/alerts', {
|
||||||
|
method: 'POST',
|
||||||
|
body: "Unknown login from 5.31.23.83 to backups.example.com",
|
||||||
|
headers: {
|
||||||
|
'Email': 'phil@example.com',
|
||||||
|
'Tags': 'warning,skull,backup-host,ssh-login',
|
||||||
|
'Priority': 'high'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Go"
|
||||||
|
``` go
|
||||||
|
req, _ := http.NewRequest("POST", "https://ntfy.sh/alerts",
|
||||||
|
strings.NewReader("Unknown login from 5.31.23.83 to backups.example.com"))
|
||||||
|
req.Header.Set("Email", "phil@example.com")
|
||||||
|
req.Header.Set("Tags", "warning,skull,backup-host,ssh-login")
|
||||||
|
req.Header.Set("Priority", "high")
|
||||||
|
http.DefaultClient.Do(req)
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Python"
|
||||||
|
``` python
|
||||||
|
requests.post("https://ntfy.sh/alerts",
|
||||||
|
data="Unknown login from 5.31.23.83 to backups.example.com",
|
||||||
|
headers={
|
||||||
|
"Email": "phil@example.com",
|
||||||
|
"Tags": "warning,skull,backup-host,ssh-login",
|
||||||
|
"Priority": "high"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "PHP"
|
||||||
|
``` php-inline
|
||||||
|
file_get_contents('https://ntfy.sh/alerts', false, stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' =>
|
||||||
|
"Content-Type: text/plain\r\n" .
|
||||||
|
"Email: phil@example.com\r\n" .
|
||||||
|
"Tags: warning,skull,backup-host,ssh-login\r\n" .
|
||||||
|
"Priority: high",
|
||||||
|
'content' => 'Unknown login from 5.31.23.83 to backups.example.com'
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's what that looks like in Google Mail:
|
||||||
|
|
||||||
|
<figure markdown>
|
||||||
|
{ width=600 }
|
||||||
|
<figcaption>E-mail notification</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
## Advanced features
|
## Advanced features
|
||||||
|
|
||||||
### Message caching
|
### Message caching
|
||||||
@@ -746,7 +845,8 @@ but just in case, let's list them all:
|
|||||||
| Limit | Description |
|
| Limit | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Message length** | Each message can be up to 512 bytes long. Longer messages are truncated. |
|
| **Message length** | Each message can be up to 512 bytes long. Longer messages are truncated. |
|
||||||
| **Requests per second** | By default, the server is configured to allow 60 requests at once, and then refills the your allowed requests bucket at a rate of one request per 10 seconds. You can read more about this in the [rate limiting](config.md#rate-limiting) section. |
|
| **Requests** | By default, the server is configured to allow 60 requests at once, and then refills the your allowed requests bucket at a rate of one request per 10 seconds. You can read more about this in the [rate limiting](config.md#rate-limiting) section. |
|
||||||
|
| **E-mails** | By default, the server is configured to allow 16 e-mails at once, and then refills the your allowed e-mail bucket at a rate of one per hour. You can read more about this in the [rate limiting](config.md#rate-limiting) section. |
|
||||||
| **Subscription limits** | By default, the server allows each visitor to keep 30 connections to the server open. |
|
| **Subscription limits** | By default, the server allows each visitor to keep 30 connections to the server open. |
|
||||||
| **Total number of topics** | By default, the server is configured to allow 5,000 topics. The ntfy.sh server has higher limits though. |
|
| **Total number of topics** | By default, the server is configured to allow 5,000 topics. The ntfy.sh server has higher limits though. |
|
||||||
|
|
||||||
@@ -761,5 +861,6 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
|
|||||||
| `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) |
|
| `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) |
|
||||||
| `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) |
|
| `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) |
|
||||||
| `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) |
|
| `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) |
|
||||||
|
| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) |
|
||||||
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
|
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
|
||||||
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
|
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
|
||||||
|
|||||||
BIN
docs/static/img/screenshot-email.png
vendored
Normal file
BIN
docs/static/img/screenshot-email.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -20,16 +20,20 @@ const (
|
|||||||
// Defines all the limits
|
// Defines all the limits
|
||||||
// - global topic limit: max number of topics overall
|
// - global topic limit: max number of topics overall
|
||||||
// - 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 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
|
||||||
const (
|
const (
|
||||||
DefaultGlobalTopicLimit = 5000
|
DefaultGlobalTopicLimit = 5000
|
||||||
DefaultVisitorRequestLimitBurst = 60
|
DefaultVisitorRequestLimitBurst = 60
|
||||||
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
||||||
|
DefaultVisitorEmailLimitBurst = 16
|
||||||
|
DefaultVisitorEmailLimitReplenish = time.Hour
|
||||||
DefaultVisitorSubscriptionLimit = 30
|
DefaultVisitorSubscriptionLimit = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
ListenHTTP string
|
ListenHTTP string
|
||||||
ListenHTTPS string
|
ListenHTTPS string
|
||||||
KeyFile string
|
KeyFile string
|
||||||
@@ -41,12 +45,18 @@ type Config struct {
|
|||||||
ManagerInterval time.Duration
|
ManagerInterval time.Duration
|
||||||
AtSenderInterval time.Duration
|
AtSenderInterval time.Duration
|
||||||
FirebaseKeepaliveInterval time.Duration
|
FirebaseKeepaliveInterval time.Duration
|
||||||
|
SMTPAddr string
|
||||||
|
SMTPUser string
|
||||||
|
SMTPPass string
|
||||||
|
SMTPFrom string
|
||||||
MessageLimit int
|
MessageLimit int
|
||||||
MinDelay time.Duration
|
MinDelay time.Duration
|
||||||
MaxDelay time.Duration
|
MaxDelay time.Duration
|
||||||
GlobalTopicLimit int
|
GlobalTopicLimit int
|
||||||
VisitorRequestLimitBurst int
|
VisitorRequestLimitBurst int
|
||||||
VisitorRequestLimitReplenish time.Duration
|
VisitorRequestLimitReplenish time.Duration
|
||||||
|
VisitorEmailLimitBurst int
|
||||||
|
VisitorEmailLimitReplenish time.Duration
|
||||||
VisitorSubscriptionLimit int
|
VisitorSubscriptionLimit int
|
||||||
BehindProxy bool
|
BehindProxy bool
|
||||||
}
|
}
|
||||||
@@ -54,6 +64,7 @@ type Config struct {
|
|||||||
// 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: "",
|
||||||
ListenHTTP: DefaultListenHTTP,
|
ListenHTTP: DefaultListenHTTP,
|
||||||
ListenHTTPS: "",
|
ListenHTTPS: "",
|
||||||
KeyFile: "",
|
KeyFile: "",
|
||||||
@@ -71,6 +82,8 @@ func NewConfig() *Config {
|
|||||||
GlobalTopicLimit: DefaultGlobalTopicLimit,
|
GlobalTopicLimit: DefaultGlobalTopicLimit,
|
||||||
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
||||||
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
||||||
|
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||||
|
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||||
BehindProxy: false,
|
BehindProxy: false,
|
||||||
}
|
}
|
||||||
|
|||||||
117
server/mailer.go
Normal file
117
server/mailer.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed" // required by go:embed
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
|
"net"
|
||||||
|
"net/smtp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mailer interface {
|
||||||
|
Send(from, to string, m *message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type smtpMailer struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *smtpMailer) Send(senderIP, to string, m *message) error {
|
||||||
|
host, _, err := net.SplitHostPort(s.config.SMTPAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message, err := formatMail(s.config.BaseURL, senderIP, s.config.SMTPFrom, to, m)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
auth := smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host)
|
||||||
|
return smtp.SendMail(s.config.SMTPAddr, auth, s.config.SMTPFrom, []string{to}, []byte(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMail(baseURL, senderIP, from, to string, m *message) (string, error) {
|
||||||
|
topicURL := baseURL + "/" + m.Topic
|
||||||
|
subject := m.Title
|
||||||
|
if subject == "" {
|
||||||
|
subject = m.Message
|
||||||
|
}
|
||||||
|
subject = strings.ReplaceAll(strings.ReplaceAll(subject, "\r", ""), "\n", " ")
|
||||||
|
message := m.Message
|
||||||
|
trailer := ""
|
||||||
|
if len(m.Tags) > 0 {
|
||||||
|
emojis, tags, err := toEmojis(m.Tags)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(emojis) > 0 {
|
||||||
|
subject = strings.Join(emojis, " ") + " " + subject
|
||||||
|
}
|
||||||
|
if len(tags) > 0 {
|
||||||
|
trailer = "Tags: " + strings.Join(tags, ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Priority != 0 && m.Priority != 3 {
|
||||||
|
priority, err := util.PriorityString(m.Priority)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if trailer != "" {
|
||||||
|
trailer += "\n"
|
||||||
|
}
|
||||||
|
trailer += fmt.Sprintf("Priority: %s", priority)
|
||||||
|
}
|
||||||
|
if trailer != "" {
|
||||||
|
message += "\n\n" + trailer
|
||||||
|
}
|
||||||
|
body := `Content-Type: text/plain; charset="utf-8"
|
||||||
|
From: "{shortTopicURL}" <{from}>
|
||||||
|
To: {to}
|
||||||
|
Subject: {subject}
|
||||||
|
|
||||||
|
{message}
|
||||||
|
|
||||||
|
--
|
||||||
|
This message was sent by {ip} at {time} via {topicURL}`
|
||||||
|
body = strings.ReplaceAll(body, "{from}", from)
|
||||||
|
body = strings.ReplaceAll(body, "{to}", to)
|
||||||
|
body = strings.ReplaceAll(body, "{subject}", subject)
|
||||||
|
body = strings.ReplaceAll(body, "{message}", message)
|
||||||
|
body = strings.ReplaceAll(body, "{topicURL}", topicURL)
|
||||||
|
body = strings.ReplaceAll(body, "{shortTopicURL}", util.ShortTopicURL(topicURL))
|
||||||
|
body = strings.ReplaceAll(body, "{time}", time.Unix(m.Time, 0).UTC().Format(time.RFC1123))
|
||||||
|
body = strings.ReplaceAll(body, "{ip}", senderIP)
|
||||||
|
return body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed "mailer_emoji.json"
|
||||||
|
emojisJSON string
|
||||||
|
)
|
||||||
|
|
||||||
|
type emoji struct {
|
||||||
|
Emoji string `json:"emoji"`
|
||||||
|
Aliases []string `json:"aliases"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func toEmojis(tags []string) (emojisOut []string, tagsOut []string, err error) {
|
||||||
|
var emojis []emoji
|
||||||
|
if err = json.Unmarshal([]byte(emojisJSON), &emojis); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
tagsOut = make([]string, 0)
|
||||||
|
emojisOut = make([]string, 0)
|
||||||
|
nextTag:
|
||||||
|
for _, t := range tags { // TODO Super inefficient; we should just create a .json file with a map
|
||||||
|
for _, e := range emojis {
|
||||||
|
if util.InStringList(e.Aliases, t) {
|
||||||
|
emojisOut = append(emojisOut, e.Emoji)
|
||||||
|
continue nextTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tagsOut = append(tagsOut, t)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
1
server/mailer_emoji.json
Normal file
1
server/mailer_emoji.json
Normal file
File diff suppressed because one or more lines are too long
141
server/mailer_test.go
Normal file
141
server/mailer_test.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatMail_Basic(t *testing.T) {
|
||||||
|
actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
|
||||||
|
ID: "abc",
|
||||||
|
Time: 1640382204,
|
||||||
|
Event: "message",
|
||||||
|
Topic: "alerts",
|
||||||
|
Message: "A simple message",
|
||||||
|
})
|
||||||
|
expected := `Content-Type: text/plain; charset="utf-8"
|
||||||
|
From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
|
||||||
|
To: phil@example.com
|
||||||
|
Subject: A simple message
|
||||||
|
|
||||||
|
A simple message
|
||||||
|
|
||||||
|
--
|
||||||
|
This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatMail_JustEmojis(t *testing.T) {
|
||||||
|
actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
|
||||||
|
ID: "abc",
|
||||||
|
Time: 1640382204,
|
||||||
|
Event: "message",
|
||||||
|
Topic: "alerts",
|
||||||
|
Message: "A simple message",
|
||||||
|
Tags: []string{"grinning"},
|
||||||
|
})
|
||||||
|
expected := `Content-Type: text/plain; charset="utf-8"
|
||||||
|
From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
|
||||||
|
To: phil@example.com
|
||||||
|
Subject: 😀 A simple message
|
||||||
|
|
||||||
|
A simple message
|
||||||
|
|
||||||
|
--
|
||||||
|
This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatMail_JustOtherTags(t *testing.T) {
|
||||||
|
actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
|
||||||
|
ID: "abc",
|
||||||
|
Time: 1640382204,
|
||||||
|
Event: "message",
|
||||||
|
Topic: "alerts",
|
||||||
|
Message: "A simple message",
|
||||||
|
Tags: []string{"not-an-emoji"},
|
||||||
|
})
|
||||||
|
expected := `Content-Type: text/plain; charset="utf-8"
|
||||||
|
From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
|
||||||
|
To: phil@example.com
|
||||||
|
Subject: A simple message
|
||||||
|
|
||||||
|
A simple message
|
||||||
|
|
||||||
|
Tags: not-an-emoji
|
||||||
|
|
||||||
|
--
|
||||||
|
This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatMail_JustPriority(t *testing.T) {
|
||||||
|
actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
|
||||||
|
ID: "abc",
|
||||||
|
Time: 1640382204,
|
||||||
|
Event: "message",
|
||||||
|
Topic: "alerts",
|
||||||
|
Message: "A simple message",
|
||||||
|
Priority: 2,
|
||||||
|
})
|
||||||
|
expected := `Content-Type: text/plain; charset="utf-8"
|
||||||
|
From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
|
||||||
|
To: phil@example.com
|
||||||
|
Subject: A simple message
|
||||||
|
|
||||||
|
A simple message
|
||||||
|
|
||||||
|
Priority: low
|
||||||
|
|
||||||
|
--
|
||||||
|
This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatMail_UTF8Subject(t *testing.T) {
|
||||||
|
actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
|
||||||
|
ID: "abc",
|
||||||
|
Time: 1640382204,
|
||||||
|
Event: "message",
|
||||||
|
Topic: "alerts",
|
||||||
|
Message: "A simple message",
|
||||||
|
Title: " :: A not so simple title öäüß ¡Hola, señor!",
|
||||||
|
})
|
||||||
|
expected := `Content-Type: text/plain; charset="utf-8"
|
||||||
|
From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
|
||||||
|
To: phil@example.com
|
||||||
|
Subject: :: A not so simple title öäüß ¡Hola, señor!
|
||||||
|
|
||||||
|
A simple message
|
||||||
|
|
||||||
|
--
|
||||||
|
This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatMail_WithAllTheThings(t *testing.T) {
|
||||||
|
actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
|
||||||
|
ID: "abc",
|
||||||
|
Time: 1640382204,
|
||||||
|
Event: "message",
|
||||||
|
Topic: "alerts",
|
||||||
|
Priority: 5,
|
||||||
|
Tags: []string{"warning", "skull", "tag123", "other"},
|
||||||
|
Title: "Oh no 🙈\nThis is a message across\nmultiple lines",
|
||||||
|
Message: "A message that contains monkeys 🙉\nNo really, though. Monkeys!",
|
||||||
|
})
|
||||||
|
expected := `Content-Type: text/plain; charset="utf-8"
|
||||||
|
From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
|
||||||
|
To: phil@example.com
|
||||||
|
Subject: ⚠️ 💀 Oh no 🙈 This is a message across multiple lines
|
||||||
|
|
||||||
|
A message that contains monkeys 🙉
|
||||||
|
No really, though. Monkeys!
|
||||||
|
|
||||||
|
Tags: tag123, other
|
||||||
|
Priority: max
|
||||||
|
|
||||||
|
--
|
||||||
|
This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"embed" // required for go:embed
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
firebase "firebase.google.com/go"
|
firebase "firebase.google.com/go"
|
||||||
"firebase.google.com/go/messaging"
|
"firebase.google.com/go/messaging"
|
||||||
@@ -33,6 +33,7 @@ type Server struct {
|
|||||||
topics map[string]*topic
|
topics map[string]*topic
|
||||||
visitors map[string]*visitor
|
visitors map[string]*visitor
|
||||||
firebase subscriber
|
firebase subscriber
|
||||||
|
mailer mailer
|
||||||
messages int64
|
messages int64
|
||||||
cache cache
|
cache cache
|
||||||
closeChan chan bool
|
closeChan chan bool
|
||||||
@@ -110,6 +111,7 @@ var (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
firebaseControlTopic = "~control" // See Android if changed
|
firebaseControlTopic = "~control" // See Android if changed
|
||||||
|
emptyMessageBody = "triggered"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New instantiates a new Server. It creates the cache and adds a Firebase
|
// New instantiates a new Server. It creates the cache and adds a Firebase
|
||||||
@@ -123,6 +125,10 @@ func New(conf *Config) (*Server, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var mailer mailer
|
||||||
|
if conf.SMTPAddr != "" {
|
||||||
|
mailer = &smtpMailer{config: conf}
|
||||||
|
}
|
||||||
cache, err := createCache(conf)
|
cache, err := createCache(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -135,6 +141,7 @@ func New(conf *Config) (*Server, error) {
|
|||||||
config: conf,
|
config: conf,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
firebase: firebaseSubscriber,
|
firebase: firebaseSubscriber,
|
||||||
|
mailer: mailer,
|
||||||
topics: topics,
|
topics: topics,
|
||||||
visitors: make(map[string]*visitor),
|
visitors: make(map[string]*visitor),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -296,7 +303,7 @@ func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visitor) error {
|
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
t, err := s.topicFromPath(r.URL.Path)
|
t, err := s.topicFromPath(r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -307,12 +314,20 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m := newDefaultMessage(t.ID, strings.TrimSpace(string(b)))
|
m := newDefaultMessage(t.ID, strings.TrimSpace(string(b)))
|
||||||
cache, firebase, err := s.parseParams(r, m)
|
cache, firebase, email, err := s.parseParams(r, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if email != "" {
|
||||||
|
if err := v.EmailAllowed(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.mailer == nil && email != "" {
|
||||||
|
return errHTTPBadRequest
|
||||||
|
}
|
||||||
if m.Message == "" {
|
if m.Message == "" {
|
||||||
m.Message = "triggered"
|
m.Message = emptyMessageBody
|
||||||
}
|
}
|
||||||
delayed := m.Time > time.Now().Unix()
|
delayed := m.Time > time.Now().Unix()
|
||||||
if !delayed {
|
if !delayed {
|
||||||
@@ -327,6 +342,13 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
if s.mailer != nil && email != "" && !delayed {
|
||||||
|
go func() {
|
||||||
|
if err := s.mailer.Send(v.ip, email, m); err != nil {
|
||||||
|
log.Printf("Unable to send email: %v", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
if cache {
|
if cache {
|
||||||
if err := s.cache.AddMessage(m); err != nil {
|
if err := s.cache.AddMessage(m); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -341,9 +363,10 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase bool, err error) {
|
func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase bool, email string, err error) {
|
||||||
cache = readParam(r, "x-cache", "cache") != "no"
|
cache = readParam(r, "x-cache", "cache") != "no"
|
||||||
firebase = readParam(r, "x-firebase", "firebase") != "no"
|
firebase = readParam(r, "x-firebase", "firebase") != "no"
|
||||||
|
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
|
||||||
m.Title = readParam(r, "x-title", "title", "t")
|
m.Title = readParam(r, "x-title", "title", "t")
|
||||||
messageStr := readParam(r, "x-message", "message", "m")
|
messageStr := readParam(r, "x-message", "message", "m")
|
||||||
if messageStr != "" {
|
if messageStr != "" {
|
||||||
@@ -351,7 +374,7 @@ func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase
|
|||||||
}
|
}
|
||||||
m.Priority, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
|
m.Priority, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, errHTTPBadRequest
|
return false, false, "", errHTTPBadRequest
|
||||||
}
|
}
|
||||||
tagsStr := readParam(r, "x-tags", "tags", "tag", "ta")
|
tagsStr := readParam(r, "x-tags", "tags", "tag", "ta")
|
||||||
if tagsStr != "" {
|
if tagsStr != "" {
|
||||||
@@ -363,19 +386,22 @@ func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase
|
|||||||
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
|
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
|
||||||
if delayStr != "" {
|
if delayStr != "" {
|
||||||
if !cache {
|
if !cache {
|
||||||
return false, false, errHTTPBadRequest
|
return false, false, "", errHTTPBadRequest
|
||||||
|
}
|
||||||
|
if email != "" {
|
||||||
|
return false, false, "", errHTTPBadRequest // we cannot store the email address (yet)
|
||||||
}
|
}
|
||||||
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, errHTTPBadRequest
|
return false, false, "", errHTTPBadRequest
|
||||||
} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
|
} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
|
||||||
return false, false, errHTTPBadRequest
|
return false, false, "", errHTTPBadRequest
|
||||||
} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
|
} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
|
||||||
return false, false, errHTTPBadRequest
|
return false, false, "", errHTTPBadRequest
|
||||||
}
|
}
|
||||||
m.Time = delay.Unix()
|
m.Time = delay.Unix()
|
||||||
}
|
}
|
||||||
return cache, firebase, nil
|
return cache, firebase, email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readParam(r *http.Request, names ...string) string {
|
func readParam(r *http.Request, names ...string) string {
|
||||||
@@ -714,6 +740,7 @@ func (s *Server) sendDelayedMessages() error {
|
|||||||
log.Printf("unable to publish to Firebase: %v", err.Error())
|
log.Printf("unable to publish to Firebase: %v", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO delayed email sending
|
||||||
}
|
}
|
||||||
if err := s.cache.MarkPublished(m); err != nil {
|
if err := s.cache.MarkPublished(m); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -745,7 +772,7 @@ func (s *Server) visitor(r *http.Request) *visitor {
|
|||||||
}
|
}
|
||||||
v, exists := s.visitors[ip]
|
v, exists := s.visitors[ip]
|
||||||
if !exists {
|
if !exists {
|
||||||
s.visitors[ip] = newVisitor(s.config)
|
s.visitors[ip] = newVisitor(s.config, ip)
|
||||||
return s.visitors[ip]
|
return s.visitors[ip]
|
||||||
}
|
}
|
||||||
v.Keepalive()
|
v.Keepalive()
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
# ntfy server config file
|
# ntfy server config file
|
||||||
|
|
||||||
|
# Public facing base URL of the service (e.g. https://ntfy.sh or https://ntfy.example.com)
|
||||||
|
# This setting is currently only used by the e-mail feature.
|
||||||
|
#
|
||||||
|
# base-url:
|
||||||
|
|
||||||
# Listen address for the HTTP & HTTPS web server. If "listen-https" is set, you must also
|
# Listen address for the HTTP & HTTPS web server. If "listen-https" is set, you must also
|
||||||
# set "key-file" and "cert-file".
|
# set "key-file" and "cert-file". Format: <hostname>:<port>
|
||||||
# Format: <hostname>:<port>
|
|
||||||
#
|
#
|
||||||
# listen-http: ":80"
|
# listen-http: ":80"
|
||||||
# listen-https:
|
# listen-https:
|
||||||
@@ -34,6 +38,27 @@
|
|||||||
#
|
#
|
||||||
# cache-duration: 12h
|
# cache-duration: 12h
|
||||||
|
|
||||||
|
# If set, the X-Forwarded-For header is used to determine the visitor IP address
|
||||||
|
# instead of the remote address of the connection.
|
||||||
|
#
|
||||||
|
# WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate limited
|
||||||
|
# as if they are one.
|
||||||
|
#
|
||||||
|
# behind-proxy: false
|
||||||
|
|
||||||
|
# If enabled, allow e-mail notifications via the 'X-Email' header. As of today, only SMTP servers
|
||||||
|
# with plain text auth and STARTLS are supported. Please also refer to the rate limiting settings
|
||||||
|
# below (visitor-email-limit-burst & visitor-email-limit-burst).
|
||||||
|
#
|
||||||
|
# - smtp-addr is the hostname:port of the SMTP server
|
||||||
|
# - smtp-user/smtp-pass are the username and password of the SMTP user
|
||||||
|
# - smtp-from is the e-mail address of the sender
|
||||||
|
#
|
||||||
|
# smtp-addr:
|
||||||
|
# smtp-user:
|
||||||
|
# smtp-pass:
|
||||||
|
# smtp-from:
|
||||||
|
|
||||||
# Interval in which keepalive messages are sent to the client. This is to prevent
|
# Interval in which keepalive messages are sent to the client. This is to prevent
|
||||||
# intermediaries closing the connection for inactivity.
|
# intermediaries closing the connection for inactivity.
|
||||||
#
|
#
|
||||||
@@ -61,10 +86,9 @@
|
|||||||
# visitor-request-limit-burst: 60
|
# visitor-request-limit-burst: 60
|
||||||
# visitor-request-limit-replenish: 10s
|
# visitor-request-limit-replenish: 10s
|
||||||
|
|
||||||
# If set, the X-Forwarded-For header is used to determine the visitor IP address
|
# Rate limiting: Allowed emails per visitor:
|
||||||
# instead of the remote address of the connection.
|
# - visitor-email-limit-burst is the initial bucket of emails each visitor has
|
||||||
|
# - visitor-email-limit-replenish is the rate at which the bucket is refilled
|
||||||
#
|
#
|
||||||
# WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate limited
|
# visitor-email-limit-burst: 16
|
||||||
# as if they are one.
|
# visitor-email-limit-replenish: 1h
|
||||||
#
|
|
||||||
# behind-proxy: false
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -508,6 +509,79 @@ func TestServer_Curl_Publish_Poll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type testMailer struct {
|
||||||
|
count int
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testMailer) Send(from, to string, m *message) error {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
t.count++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishTooManyEmails_Defaults(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
s.mailer = &testMailer{}
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{
|
||||||
|
"E-Mail": "test@example.com",
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
}
|
||||||
|
response := request(t, s, "PUT", "/mytopic", "one too many", map[string]string{
|
||||||
|
"E-Mail": "test@example.com",
|
||||||
|
})
|
||||||
|
require.Equal(t, 429, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishTooManyEmails_Replenish(t *testing.T) {
|
||||||
|
c := newTestConfig(t)
|
||||||
|
c.VisitorEmailLimitReplenish = 500 * time.Millisecond
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
s.mailer = &testMailer{}
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
response := request(t, s, "PUT", "/mytopic", fmt.Sprintf("message %d", i), map[string]string{
|
||||||
|
"E-Mail": "test@example.com",
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
}
|
||||||
|
response := request(t, s, "PUT", "/mytopic", "one too many", map[string]string{
|
||||||
|
"E-Mail": "test@example.com",
|
||||||
|
})
|
||||||
|
require.Equal(t, 429, response.Code)
|
||||||
|
|
||||||
|
time.Sleep(510 * time.Millisecond)
|
||||||
|
response = request(t, s, "PUT", "/mytopic", "this should be okay again too many", map[string]string{
|
||||||
|
"E-Mail": "test@example.com",
|
||||||
|
})
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
|
||||||
|
response = request(t, s, "PUT", "/mytopic", "and bad again", map[string]string{
|
||||||
|
"E-Mail": "test@example.com",
|
||||||
|
})
|
||||||
|
require.Equal(t, 429, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishDelayedEmail_Fail(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
s.mailer = &testMailer{}
|
||||||
|
response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{
|
||||||
|
"E-Mail": "test@example.com",
|
||||||
|
"Delay": "20 min",
|
||||||
|
})
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PublishEmailNoMailer_Fail(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{
|
||||||
|
"E-Mail": "test@example.com",
|
||||||
|
})
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func newTestConfig(t *testing.T) *Config {
|
func newTestConfig(t *testing.T) *Config {
|
||||||
conf := NewConfig()
|
conf := NewConfig()
|
||||||
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
||||||
|
|||||||
@@ -8,29 +8,47 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
visitorExpungeAfter = 30 * time.Minute
|
// visitorExpungeAfter defines how long a visitor is active before it is removed from memory. This number
|
||||||
|
// has to be very high to prevent e-mail abuse, but it doesn't really affect the other limits anyway, since
|
||||||
|
// they are replenished faster (typically).
|
||||||
|
visitorExpungeAfter = 24 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// visitor represents an API user, and its associated rate.Limiter used for rate limiting
|
// visitor represents an API user, and its associated rate.Limiter used for rate limiting
|
||||||
type visitor struct {
|
type visitor struct {
|
||||||
config *Config
|
config *Config
|
||||||
limiter *rate.Limiter
|
ip string
|
||||||
|
requests *rate.Limiter
|
||||||
|
emails *rate.Limiter
|
||||||
subscriptions *util.Limiter
|
subscriptions *util.Limiter
|
||||||
seen time.Time
|
seen time.Time
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newVisitor(conf *Config) *visitor {
|
func newVisitor(conf *Config, ip string) *visitor {
|
||||||
return &visitor{
|
return &visitor{
|
||||||
config: conf,
|
config: conf,
|
||||||
limiter: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
ip: ip,
|
||||||
|
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
||||||
|
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
|
||||||
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
|
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
|
||||||
seen: time.Now(),
|
seen: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *visitor) IP() string {
|
||||||
|
return v.ip
|
||||||
|
}
|
||||||
|
|
||||||
func (v *visitor) RequestAllowed() error {
|
func (v *visitor) RequestAllowed() error {
|
||||||
if !v.limiter.Allow() {
|
if !v.requests.Allow() {
|
||||||
|
return errHTTPTooManyRequests
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *visitor) EmailAllowed() error {
|
||||||
|
if !v.emails.Allow() {
|
||||||
return errHTTPTooManyRequests
|
return errHTTPTooManyRequests
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rand.Seed(time.Now().Unix())
|
rand.Seed(time.Now().UnixMilli())
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartServer starts a server.Server with a random port and waits for the server to be up
|
// StartServer starts a server.Server with a random port and waits for the server to be up
|
||||||
func StartServer(t *testing.T) (*server.Server, int) {
|
func StartServer(t *testing.T) (*server.Server, int) {
|
||||||
|
return StartServerWithConfig(t, server.NewConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartServerWithConfig starts a server.Server with a random port and waits for the server to be up
|
||||||
|
func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.Server, int) {
|
||||||
port := 10000 + rand.Intn(20000)
|
port := 10000 + rand.Intn(20000)
|
||||||
conf := server.NewConfig()
|
|
||||||
conf.ListenHTTP = fmt.Sprintf(":%d", port)
|
conf.ListenHTTP = fmt.Sprintf(":%d", port)
|
||||||
s, err := server.New(conf)
|
s, err := server.New(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
25
util/util.go
25
util/util.go
@@ -134,7 +134,32 @@ func ParsePriority(priority string) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PriorityString converts a priority number to a string
|
||||||
|
func PriorityString(priority int) (string, error) {
|
||||||
|
switch priority {
|
||||||
|
case 0:
|
||||||
|
return "default", nil
|
||||||
|
case 1:
|
||||||
|
return "min", nil
|
||||||
|
case 2:
|
||||||
|
return "low", nil
|
||||||
|
case 3:
|
||||||
|
return "default", nil
|
||||||
|
case 4:
|
||||||
|
return "high", nil
|
||||||
|
case 5:
|
||||||
|
return "max", nil
|
||||||
|
default:
|
||||||
|
return "", errInvalidPriority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ExpandHome replaces "~" with the user's home directory
|
// ExpandHome replaces "~" with the user's home directory
|
||||||
func ExpandHome(path string) string {
|
func ExpandHome(path string) string {
|
||||||
return os.ExpandEnv(strings.ReplaceAll(path, "~", "$HOME"))
|
return os.ExpandEnv(strings.ReplaceAll(path, "~", "$HOME"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShortTopicURL shortens the topic URL to be human-friendly, removing the http:// or https://
|
||||||
|
func ShortTopicURL(s string) string {
|
||||||
|
return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://")
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,3 +100,24 @@ func TestParsePriority_Invalid(t *testing.T) {
|
|||||||
require.Equal(t, errInvalidPriority, err)
|
require.Equal(t, errInvalidPriority, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPriorityString(t *testing.T) {
|
||||||
|
priorities := []int{0, 1, 2, 3, 4, 5}
|
||||||
|
expected := []string{"default", "min", "low", "default", "high", "max"}
|
||||||
|
for i, priority := range priorities {
|
||||||
|
actual, err := PriorityString(priority)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, expected[i], actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPriorityString_Invalid(t *testing.T) {
|
||||||
|
_, err := PriorityString(99)
|
||||||
|
require.Equal(t, err, errInvalidPriority)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShortTopicURL(t *testing.T) {
|
||||||
|
require.Equal(t, "ntfy.sh/mytopic", ShortTopicURL("https://ntfy.sh/mytopic"))
|
||||||
|
require.Equal(t, "ntfy.sh/mytopic", ShortTopicURL("http://ntfy.sh/mytopic"))
|
||||||
|
require.Equal(t, "lalala", ShortTopicURL("lalala"))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user