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.True(t, time.Now().Add(24*time.Hour).Unix() < msg.Time)
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
msg = nextMessage(c)
|
||||
require.NotNil(t, msg)
|
||||
require.Equal(t, "some message", msg.Message)
|
||||
|
||||
@@ -45,6 +45,11 @@ func WithDelay(delay string) PublishOption {
|
||||
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
|
||||
func WithNoCache() PublishOption {
|
||||
return WithHeader("X-Cache", "no")
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/util"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -60,7 +59,3 @@ func initConfigFileInputSource(configFlag string, flags []cli.Flag) cli.BeforeFu
|
||||
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 (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/client"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -24,3 +27,11 @@ func newTestApp() (*cli.App, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
|
||||
app.ErrWriter = &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: "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: "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-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"},
|
||||
&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 --delay=10s delayed_topic Laterzz # Delay message by 10s
|
||||
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
|
||||
|
||||
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")
|
||||
tags := c.String("tags")
|
||||
delay := c.String("delay")
|
||||
email := c.String("email")
|
||||
noCache := c.Bool("no-cache")
|
||||
noFirebase := c.Bool("no-firebase")
|
||||
quiet := c.Bool("quiet")
|
||||
@@ -75,6 +78,9 @@ func execPublish(c *cli.Context) error {
|
||||
if delay != "" {
|
||||
options = append(options, client.WithDelay(delay))
|
||||
}
|
||||
if email != "" {
|
||||
options = append(options, client.WithEmail(email))
|
||||
}
|
||||
if noCache {
|
||||
options = append(options, client.WithNoCache())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/util"
|
||||
"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.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
|
||||
|
||||
import (
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
|
||||
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"},
|
||||
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-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"}),
|
||||
@@ -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: "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.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: "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-request-limit-burst", Aliases: []string{"B"}, 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.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", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.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)"}),
|
||||
}
|
||||
|
||||
@@ -52,6 +58,7 @@ func execServe(c *cli.Context) error {
|
||||
}
|
||||
|
||||
// Read all the options
|
||||
baseURL := c.String("base-url")
|
||||
listenHTTP := c.String("listen-http")
|
||||
listenHTTPS := c.String("listen-https")
|
||||
keyFile := c.String("key-file")
|
||||
@@ -61,10 +68,16 @@ func execServe(c *cli.Context) error {
|
||||
cacheDuration := c.Duration("cache-duration")
|
||||
keepaliveInterval := c.Duration("keepalive-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")
|
||||
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
|
||||
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
|
||||
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")
|
||||
|
||||
// Check values
|
||||
@@ -82,10 +95,13 @@ func execServe(c *cli.Context) error {
|
||||
return errors.New("if set, certificate file must exist")
|
||||
} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
|
||||
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
|
||||
conf := server.NewConfig()
|
||||
conf.BaseURL = baseURL
|
||||
conf.ListenHTTP = listenHTTP
|
||||
conf.ListenHTTPS = listenHTTPS
|
||||
conf.KeyFile = keyFile
|
||||
@@ -95,10 +111,16 @@ func execServe(c *cli.Context) error {
|
||||
conf.CacheDuration = cacheDuration
|
||||
conf.KeepaliveInterval = keepaliveInterval
|
||||
conf.ManagerInterval = managerInterval
|
||||
conf.SMTPAddr = smtpAddr
|
||||
conf.SMTPUser = smtpUser
|
||||
conf.SMTPPass = smtpPass
|
||||
conf.SMTPFrom = smtpFrom
|
||||
conf.GlobalTopicLimit = globalTopicLimit
|
||||
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
||||
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
|
||||
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
|
||||
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
||||
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
||||
conf.BehindProxy = behindProxy
|
||||
s, err := server.New(conf)
|
||||
if err != nil {
|
||||
|
||||
@@ -180,7 +180,7 @@ func runCommandInternal(c *cli.Context, command string, m *client.Message) error
|
||||
defer os.Remove(scriptFile)
|
||||
verbose := c.Bool("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.Stdin = c.App.Reader
|
||||
|
||||
105
docs/config.md
105
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
|
||||
[`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.)
|
||||
!!! warning
|
||||
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.
|
||||
|
||||
It may be desirable to run ntfy behind a proxy, e.g. so you can provide TLS certificates using Let's Encrypt using certbot,
|
||||
or simply because you'd like to share the ports (80/443) with other services. Whatever your reasons may be, there are a
|
||||
few things to consider.
|
||||
It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache), so you can provide TLS certificates
|
||||
using Let's Encrypt using certbot, or simply because you'd like to share the ports (80/443) with other services.
|
||||
Whatever your reasons may be, there are a few things to consider.
|
||||
|
||||
### Rate limiting
|
||||
If you are running ntfy behind a proxy (e.g. nginx, HAproxy or Apache), you should set the `behind-proxy`
|
||||
flag. This will instruct the [rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary
|
||||
identifier for a visitor, as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will
|
||||
If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. This will instruct the
|
||||
[rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary 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.
|
||||
|
||||
=== "/etc/ntfy/server.yml"
|
||||
@@ -214,7 +229,7 @@ firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
|
||||
## Rate limiting
|
||||
!!! info
|
||||
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.
|
||||
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-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
|
||||
reconnect after a connection drop), it shouldn't have any effect.
|
||||
Similarly to the request limit, there is also an e-mail limit (only relevant if [e-mail notifications](#e-mail-notifications)
|
||||
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
|
||||
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 |
|
||||
|---|---|---|---|---|
|
||||
| `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-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. |
|
||||
@@ -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). |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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-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.
|
||||
|
||||
## Command line options
|
||||
```
|
||||
$ ntfy --help
|
||||
$ ntfy serve --help
|
||||
NAME:
|
||||
ntfy - Simple pub-sub notification service
|
||||
ntfy serve - Run the ntfy server
|
||||
|
||||
USAGE:
|
||||
ntfy [OPTION..]
|
||||
ntfy serve [OPTIONS..]
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
|
||||
--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]
|
||||
DESCRIPTION:
|
||||
Run the ntfy server and listen for incoming requests
|
||||
|
||||
The command will load the configuration from /etc/ntfy/server.yml. Config options can
|
||||
be overridden using the command line options.
|
||||
|
||||
Examples:
|
||||
ntfy serve # Starts server in the foreground (on port 80)
|
||||
ntfy serve --listen-http :8080 # Starts server with alternate port
|
||||
|
||||
Try 'ntfy COMMAND --help' for more information.
|
||||
|
||||
ntfy v1.4.8 (7b8185c), runtime go1.17, built at 1637872539
|
||||
Copyright (C) 2021 Philipp C. Heckel, distributed under the Apache License 2.0
|
||||
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"
|
||||
```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 ./ntfy serve
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```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 ./ntfy serve
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```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 ./ntfy serve
|
||||
```
|
||||
@@ -88,7 +88,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```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 systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -96,7 +96,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv7/armhf"
|
||||
```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 systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -104,7 +104,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "arm64"
|
||||
```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 systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -114,21 +114,21 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```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 start ntfy
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```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 start ntfy
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```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 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');
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
### Message caching
|
||||
@@ -746,7 +845,8 @@ but just in case, let's list them all:
|
||||
| Limit | Description |
|
||||
|---|---|
|
||||
| **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. |
|
||||
| **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-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-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-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
|
||||
// - 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 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
|
||||
const (
|
||||
DefaultGlobalTopicLimit = 5000
|
||||
DefaultVisitorRequestLimitBurst = 60
|
||||
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
||||
DefaultVisitorEmailLimitBurst = 16
|
||||
DefaultVisitorEmailLimitReplenish = time.Hour
|
||||
DefaultVisitorSubscriptionLimit = 30
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -41,12 +45,18 @@ type Config struct {
|
||||
ManagerInterval time.Duration
|
||||
AtSenderInterval time.Duration
|
||||
FirebaseKeepaliveInterval time.Duration
|
||||
SMTPAddr string
|
||||
SMTPUser string
|
||||
SMTPPass string
|
||||
SMTPFrom string
|
||||
MessageLimit int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
GlobalTopicLimit int
|
||||
VisitorRequestLimitBurst int
|
||||
VisitorRequestLimitReplenish time.Duration
|
||||
VisitorEmailLimitBurst int
|
||||
VisitorEmailLimitReplenish time.Duration
|
||||
VisitorSubscriptionLimit int
|
||||
BehindProxy bool
|
||||
}
|
||||
@@ -54,6 +64,7 @@ type Config struct {
|
||||
// NewConfig instantiates a default new server config
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
BaseURL: "",
|
||||
ListenHTTP: DefaultListenHTTP,
|
||||
ListenHTTPS: "",
|
||||
KeyFile: "",
|
||||
@@ -71,6 +82,8 @@ func NewConfig() *Config {
|
||||
GlobalTopicLimit: DefaultGlobalTopicLimit,
|
||||
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
||||
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
||||
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed" // required for go:embed
|
||||
"embed"
|
||||
"encoding/json"
|
||||
firebase "firebase.google.com/go"
|
||||
"firebase.google.com/go/messaging"
|
||||
@@ -33,6 +33,7 @@ type Server struct {
|
||||
topics map[string]*topic
|
||||
visitors map[string]*visitor
|
||||
firebase subscriber
|
||||
mailer mailer
|
||||
messages int64
|
||||
cache cache
|
||||
closeChan chan bool
|
||||
@@ -110,6 +111,7 @@ var (
|
||||
|
||||
const (
|
||||
firebaseControlTopic = "~control" // See Android if changed
|
||||
emptyMessageBody = "triggered"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
var mailer mailer
|
||||
if conf.SMTPAddr != "" {
|
||||
mailer = &smtpMailer{config: conf}
|
||||
}
|
||||
cache, err := createCache(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -135,6 +141,7 @@ func New(conf *Config) (*Server, error) {
|
||||
config: conf,
|
||||
cache: cache,
|
||||
firebase: firebaseSubscriber,
|
||||
mailer: mailer,
|
||||
topics: topics,
|
||||
visitors: make(map[string]*visitor),
|
||||
}, nil
|
||||
@@ -296,7 +303,7 @@ func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -307,12 +314,20 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if email != "" {
|
||||
if err := v.EmailAllowed(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if s.mailer == nil && email != "" {
|
||||
return errHTTPBadRequest
|
||||
}
|
||||
if m.Message == "" {
|
||||
m.Message = "triggered"
|
||||
m.Message = emptyMessageBody
|
||||
}
|
||||
delayed := m.Time > time.Now().Unix()
|
||||
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 err := s.cache.AddMessage(m); err != nil {
|
||||
return err
|
||||
@@ -341,9 +363,10 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visito
|
||||
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"
|
||||
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")
|
||||
messageStr := readParam(r, "x-message", "message", "m")
|
||||
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"))
|
||||
if err != nil {
|
||||
return false, false, errHTTPBadRequest
|
||||
return false, false, "", errHTTPBadRequest
|
||||
}
|
||||
tagsStr := readParam(r, "x-tags", "tags", "tag", "ta")
|
||||
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")
|
||||
if delayStr != "" {
|
||||
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())
|
||||
if err != nil {
|
||||
return false, false, errHTTPBadRequest
|
||||
return false, false, "", errHTTPBadRequest
|
||||
} 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() {
|
||||
return false, false, errHTTPBadRequest
|
||||
return false, false, "", errHTTPBadRequest
|
||||
}
|
||||
m.Time = delay.Unix()
|
||||
}
|
||||
return cache, firebase, nil
|
||||
return cache, firebase, email, nil
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
// TODO delayed email sending
|
||||
}
|
||||
if err := s.cache.MarkPublished(m); err != nil {
|
||||
return err
|
||||
@@ -745,7 +772,7 @@ func (s *Server) visitor(r *http.Request) *visitor {
|
||||
}
|
||||
v, exists := s.visitors[ip]
|
||||
if !exists {
|
||||
s.visitors[ip] = newVisitor(s.config)
|
||||
s.visitors[ip] = newVisitor(s.config, ip)
|
||||
return s.visitors[ip]
|
||||
}
|
||||
v.Keepalive()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# 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
|
||||
# set "key-file" and "cert-file".
|
||||
# Format: <hostname>:<port>
|
||||
# set "key-file" and "cert-file". Format: <hostname>:<port>
|
||||
#
|
||||
# listen-http: ":80"
|
||||
# listen-https:
|
||||
@@ -34,6 +38,27 @@
|
||||
#
|
||||
# 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
|
||||
# intermediaries closing the connection for inactivity.
|
||||
#
|
||||
@@ -61,10 +86,9 @@
|
||||
# visitor-request-limit-burst: 60
|
||||
# visitor-request-limit-replenish: 10s
|
||||
|
||||
# If set, the X-Forwarded-For header is used to determine the visitor IP address
|
||||
# instead of the remote address of the connection.
|
||||
# Rate limiting: Allowed emails per visitor:
|
||||
# - 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
|
||||
# as if they are one.
|
||||
#
|
||||
# behind-proxy: false
|
||||
# visitor-email-limit-burst: 16
|
||||
# visitor-email-limit-replenish: 1h
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"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 {
|
||||
conf := NewConfig()
|
||||
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
||||
|
||||
@@ -8,29 +8,47 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
type visitor struct {
|
||||
config *Config
|
||||
limiter *rate.Limiter
|
||||
ip string
|
||||
requests *rate.Limiter
|
||||
emails *rate.Limiter
|
||||
subscriptions *util.Limiter
|
||||
seen time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newVisitor(conf *Config) *visitor {
|
||||
func newVisitor(conf *Config, ip string) *visitor {
|
||||
return &visitor{
|
||||
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)),
|
||||
seen: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *visitor) IP() string {
|
||||
return v.ip
|
||||
}
|
||||
|
||||
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 nil
|
||||
|
||||
@@ -10,13 +10,17 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
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)
|
||||
conf := server.NewConfig()
|
||||
conf.ListenHTTP = fmt.Sprintf(":%d", port)
|
||||
s, err := server.New(conf)
|
||||
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
|
||||
func ExpandHome(path string) string {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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