binwiederhier пре 2 година
родитељ
комит
25d3a66f91
9 измењених фајлова са 144 додато и 50 уклоњено
  1. 3 0
      cmd/serve.go
  2. 6 0
      docs/config.md
  3. 30 30
      docs/install.md
  4. 26 16
      docs/releases.md
  5. 2 0
      server/config.go
  6. 4 0
      server/server.go
  7. 5 0
      server/server.yml
  8. 62 1
      server/server_test.go
  9. 6 3
      server/server_twilio.go

+ 3 - 0
cmd/serve.go

@@ -64,6 +64,7 @@ var flagsServe = append(
 	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
 	altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
+	altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
@@ -148,6 +149,7 @@ func execServe(c *cli.Context) error {
 	enableLogin := c.Bool("enable-login")
 	enableReservations := c.Bool("enable-reservations")
 	upstreamBaseURL := c.String("upstream-base-url")
+	upstreamAccessToken := c.String("upstream-access-token")
 	smtpSenderAddr := c.String("smtp-sender-addr")
 	smtpSenderUser := c.String("smtp-sender-user")
 	smtpSenderPass := c.String("smtp-sender-pass")
@@ -311,6 +313,7 @@ func execServe(c *cli.Context) error {
 	conf.DisallowedTopics = disallowedTopics
 	conf.WebRoot = webRoot
 	conf.UpstreamBaseURL = upstreamBaseURL
+	conf.UpstreamAccessToken = upstreamAccessToken
 	conf.SMTPSenderAddr = smtpSenderAddr
 	conf.SMTPSenderUser = smtpSenderUser
 	conf.SMTPSenderPass = smtpSenderPass

+ 6 - 0
docs/config.md

@@ -759,6 +759,7 @@ To configure it, simply set `upstream-base-url` like so:
 
 ``` yaml
 upstream-base-url: "https://ntfy.sh"
+upstream-access-token: "..." # optional, only if rate limits exceeded, or upstream server protected
 ```
 
 If set, all incoming messages will publish a poll request to the configured upstream server, containing
@@ -1258,10 +1259,15 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
 | `smtp-server-listen`                       | `NTFY_SMTP_SERVER_LISTEN`                       | `[ip]:port`                                         | -                 | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25`                                                                                                                                      |
 | `smtp-server-domain`                       | `NTFY_SMTP_SERVER_DOMAIN`                       | *domain name*                                       | -                 | SMTP server e-mail domain, e.g. `ntfy.sh`                                                                                                                                                                                       |
 | `smtp-server-addr-prefix`                  | `NTFY_SMTP_SERVER_ADDR_PREFIX`                  | *string*                                            | -                 | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-`                                                                                                                                                          |
+| `twilio-account`                           | `NTFY_TWILIO_ACCOUNT`                           | *string*                                            | -                 | Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586                                                                                                                                                                     |
+| `twilio-auth-token`                        | `NTFY_TWILIO_AUTH_TOKEN`                        | *string*                                            | -                 | Twilio auth token, e.g. affebeef258625862586258625862586                                                                                                                                                                        |
+| `twilio-from-number`                       | `NTFY_TWILIO_FROM_NUMBER`                       | *string*                                            | -                 | Twilio outgoing phone number, e.g. +18775132586                                                                                                                                                                                 |
+| `twilio-verify-service`                    | `NTFY_TWILIO_VERIFY_SERVICE`                    | *string*                                            | -                 | Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586                                                                                                                                                              |
 | `keepalive-interval`                       | `NTFY_KEEPALIVE_INTERVAL`                       | *duration*                                          | 45s               | 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*                                            | 15,000            | Rate limiting: Total number of topics before the server rejects new topics.                                                                                                                                                     |
 | `upstream-base-url`                        | `NTFY_UPSTREAM_BASE_URL`                        | *URL*                                               | `https://ntfy.sh` | Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers                                                                                                                   |
+| `upstream-access-token`                    | `NTFY_UPSTREAM_ACCESS_TOKEN`                    | *string*                                            | `tk_zyYLYj...`    | Access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth                                                                                                  |
 | `visitor-attachment-total-size-limit`      | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT`      | *size*                                              | 100M              | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`.                                                 |
 | `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size*                                              | 500M              | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding.                                                                                        |
 | `visitor-email-limit-burst`                | `NTFY_VISITOR_EMAIL_LIMIT_BURST`                | *number*                                            | 16                | Rate limiting:Initial limit of e-mails per visitor                                                                                                                                                                              |

+ 30 - 30
docs/install.md

@@ -29,37 +29,37 @@ deb/rpm packages.
 
 === "x86_64/amd64"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_x86_64.tar.gz
-    tar zxvf ntfy_2.4.0_linux_x86_64.tar.gz
-    sudo cp -a ntfy_2.4.0_linux_x86_64/ntfy /usr/local/bin/ntfy
-    sudo mkdir /etc/ntfy && sudo cp ntfy_2.4.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
+    wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_x86_64.tar.gz
+    tar zxvf ntfy_2.5.0_linux_x86_64.tar.gz
+    sudo cp -a ntfy_2.5.0_linux_x86_64/ntfy /usr/local/bin/ntfy
+    sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
     sudo ntfy serve
     ```
 
 === "armv6"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_armv6.tar.gz
-    tar zxvf ntfy_2.4.0_linux_armv6.tar.gz
-    sudo cp -a ntfy_2.4.0_linux_armv6/ntfy /usr/bin/ntfy
-    sudo mkdir /etc/ntfy && sudo cp ntfy_2.4.0_linux_armv6/{client,server}/*.yml /etc/ntfy
+    wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.tar.gz
+    tar zxvf ntfy_2.5.0_linux_armv6.tar.gz
+    sudo cp -a ntfy_2.5.0_linux_armv6/ntfy /usr/bin/ntfy
+    sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_armv6/{client,server}/*.yml /etc/ntfy
     sudo ntfy serve
     ```
 
 === "armv7/armhf"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_armv7.tar.gz
-    tar zxvf ntfy_2.4.0_linux_armv7.tar.gz
-    sudo cp -a ntfy_2.4.0_linux_armv7/ntfy /usr/bin/ntfy
-    sudo mkdir /etc/ntfy && sudo cp ntfy_2.4.0_linux_armv7/{client,server}/*.yml /etc/ntfy
+    wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.tar.gz
+    tar zxvf ntfy_2.5.0_linux_armv7.tar.gz
+    sudo cp -a ntfy_2.5.0_linux_armv7/ntfy /usr/bin/ntfy
+    sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_armv7/{client,server}/*.yml /etc/ntfy
     sudo ntfy serve
     ```
 
 === "arm64"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_arm64.tar.gz
-    tar zxvf ntfy_2.4.0_linux_arm64.tar.gz
-    sudo cp -a ntfy_2.4.0_linux_arm64/ntfy /usr/bin/ntfy
-    sudo mkdir /etc/ntfy && sudo cp ntfy_2.4.0_linux_arm64/{client,server}/*.yml /etc/ntfy
+    wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.tar.gz
+    tar zxvf ntfy_2.5.0_linux_arm64.tar.gz
+    sudo cp -a ntfy_2.5.0_linux_arm64/ntfy /usr/bin/ntfy
+    sudo mkdir /etc/ntfy && sudo cp ntfy_2.5.0_linux_arm64/{client,server}/*.yml /etc/ntfy
     sudo ntfy serve
     ```
 
@@ -109,7 +109,7 @@ Manually installing the .deb file:
 
 === "x86_64/amd64"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_amd64.deb
+    wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_amd64.deb
     sudo dpkg -i ntfy_*.deb
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
@@ -117,7 +117,7 @@ Manually installing the .deb file:
 
 === "armv6"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_armv6.deb
+    wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.deb
     sudo dpkg -i ntfy_*.deb
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
@@ -125,7 +125,7 @@ Manually installing the .deb file:
 
 === "armv7/armhf"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_armv7.deb
+    wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.deb
     sudo dpkg -i ntfy_*.deb
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
@@ -133,7 +133,7 @@ Manually installing the .deb file:
 
 === "arm64"
     ```bash
-    wget https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_arm64.deb
+    wget https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.deb
     sudo dpkg -i ntfy_*.deb
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
@@ -143,28 +143,28 @@ Manually installing the .deb file:
 
 === "x86_64/amd64"
     ```bash
-    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_amd64.rpm
+    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_amd64.rpm
     sudo systemctl enable ntfy 
     sudo systemctl start ntfy
     ```
 
 === "armv6"
     ```bash
-    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_armv6.rpm
+    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv6.rpm
     sudo systemctl enable ntfy
     sudo systemctl start ntfy
     ```
 
 === "armv7/armhf"
     ```bash
-    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_armv7.rpm
+    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_armv7.rpm
     sudo systemctl enable ntfy 
     sudo systemctl start ntfy
     ```
 
 === "arm64"
     ```bash
-    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_linux_arm64.rpm
+    sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_linux_arm64.rpm
     sudo systemctl enable ntfy 
     sudo systemctl start ntfy
     ```
@@ -192,18 +192,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
 
 ## macOS
 The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. 
-To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_macOS_all.tar.gz), 
+To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_macOS_all.tar.gz), 
 extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). 
 
 If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at 
 `~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
 
 ```bash
-curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_macOS_all.tar.gz > ntfy_2.4.0_macOS_all.tar.gz
-tar zxvf ntfy_2.4.0_macOS_all.tar.gz
-sudo cp -a ntfy_2.4.0_macOS_all/ntfy /usr/local/bin/ntfy
+curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_macOS_all.tar.gz > ntfy_2.5.0_macOS_all.tar.gz
+tar zxvf ntfy_2.5.0_macOS_all.tar.gz
+sudo cp -a ntfy_2.5.0_macOS_all/ntfy /usr/local/bin/ntfy
 mkdir ~/Library/Application\ Support/ntfy 
-cp ntfy_2.4.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
+cp ntfy_2.5.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
 ntfy --help
 ```
 
@@ -221,7 +221,7 @@ brew install ntfy
 
 ## Windows
 The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
-To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.4.0/ntfy_2.4.0_windows_x86_64.zip),
+To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.5.0/ntfy_2.5.0_windows_x86_64.zip),
 extract it and place the `ntfy.exe` binary somewhere in your `%Path%`. 
 
 The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).

+ 26 - 16
docs/releases.md

@@ -2,6 +2,32 @@
 Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
 and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
 
+## ntfy server v2.5.0
+Released May 18, 2023
+
+This release brings a number of new features, including support for text-to-speech style [phone calls](publish.md#phone-calls), 
+an admin API to manage users and ACL (currently in beta, and hence undocumented), and support for authorized access to 
+upstream servers via the `upstream-access-token` config option.
+
+❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
+and [Liberapay](https://en.liberapay.com/ntfy/), or by buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
+if you use promo code `MYTOPIC`). ntfy will always remain open source.
+
+**Features:**
+
+* Support for text-to-speech style [phone calls](publish.md#phone-calls) using the `X-Call` header (no ticket)
+* Admin API to manage users and ACL, `v1/users` + `v1/users/access` (intentionally undocumented as of now, [#722](https://github.com/binwiederhier/ntfy/issues/722), thanks to [@CreativeWarlock](https://github.com/CreativeWarlock) for sponsoring this ticket)
+* Added `upstream-access-token` config option to allow authorized access to upstream servers (no ticket)
+
+**Bug fixes + maintenance:**
+
+* Removed old ntfy website from ntfy entirely (no ticket)
+* Make emoji lookup for emails more efficient ([#725](https://github.com/binwiederhier/ntfy/pull/725), thanks to [@adamantike](https://github.com/adamantike))
+* Fix potential subscriber ID clash ([#712](https://github.com/binwiederhier/ntfy/issues/712), thanks to [@peterbourgon](https://github.com/peterbourgon) for reporting, and [@dropdevrahul](https://github.com/dropdevrahul) for fixing)
+* Support for `quoted-printable` in incoming emails ([#719](https://github.com/binwiederhier/ntfy/pull/719), thanks to [@Aerion](https://github.com/Aerion))
+* Attachments with filenames that are downloaded using a browser will now download with the proper filename ([#726](https://github.com/binwiederhier/ntfy/issues/726), thanks to [@un99known99](https://github.com/un99known99) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
+* Fix web app i18n issue in account preferences ([#730](https://github.com/binwiederhier/ntfy/issues/730), thanks to [@codebude](https://github.com/codebude) for reporting)
+
 ### ntfy server v2.4.0
 Released Apr 26, 2023
 
@@ -1178,22 +1204,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 
 ## Not released yet
 
-### ntfy server v2.5.0 (UNRELEASED)
-
-**Features:**
-
-* Support for text-to-speech style [phone calls](publish.md#phone-calls) using the `X-Call` header (no ticket)
-* Admin API to manage users and ACL, `v1/users` + `v1/users/access` ([#722](https://github.com/binwiederhier/ntfy/issues/722), thanks to [@CreativeWarlock](https://github.com/CreativeWarlock) for sponsoring this ticket)
-
-**Bug fixes + maintenance:**
-
-* Removed old ntfy website from ntfy entirely (no ticket)
-* Make emoji lookup for emails more efficient ([#725](https://github.com/binwiederhier/ntfy/pull/725), thanks to [@adamantike](https://github.com/adamantike))
-* Fix potential subscriber ID clash ([#712](https://github.com/binwiederhier/ntfy/issues/712), thanks to [@peterbourgon](https://github.com/peterbourgon) for reporting, and [@dropdevrahul](https://github.com/dropdevrahul) for fixing)
-* Support for `quoted-printable` in incoming emails ([#719](https://github.com/binwiederhier/ntfy/pull/719), thanks to [@Aerion](https://github.com/Aerion))
-* Attachments with filenames that are downloaded using a browser will now download with the proper filename ([#726](https://github.com/binwiederhier/ntfy/issues/726), thanks to [@un99known99](https://github.com/un99known99) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
-* Fix web app i18n issue in account preferences ([#730](https://github.com/binwiederhier/ntfy/issues/730), thanks to [@codebude](https://github.com/codebude) for reporting)
-
 ### ntfy Android app v1.16.1 (UNRELEASED)
 
 **Features:**

+ 2 - 0
server/config.go

@@ -98,6 +98,7 @@ type Config struct {
 	FirebasePollInterval                 time.Duration
 	FirebaseQuotaExceededPenaltyDuration time.Duration
 	UpstreamBaseURL                      string
+	UpstreamAccessToken                  string
 	SMTPSenderAddr                       string
 	SMTPSenderUser                       string
 	SMTPSenderPass                       string
@@ -182,6 +183,7 @@ func NewConfig() *Config {
 		FirebasePollInterval:                 DefaultFirebasePollInterval,
 		FirebaseQuotaExceededPenaltyDuration: DefaultFirebaseQuotaExceededPenaltyDuration,
 		UpstreamBaseURL:                      "",
+		UpstreamAccessToken:                  "",
 		SMTPSenderAddr:                       "",
 		SMTPSenderUser:                       "",
 		SMTPSenderPass:                       "",

+ 4 - 0
server/server.go

@@ -855,7 +855,11 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
 		logvm(v, m).Err(err).Warn("Unable to publish poll request")
 		return
 	}
+	req.Header.Set("User-Agent", "ntfy/"+s.config.Version)
 	req.Header.Set("X-Poll-ID", m.ID)
+	if s.config.UpstreamAccessToken != "" {
+		req.Header.Set("Authorization", util.BearerAuth(s.config.UpstreamAccessToken))
+	}
 	var httpClient = &http.Client{
 		Timeout: time.Second * 10,
 	}

+ 5 - 0
server/server.yml

@@ -208,7 +208,12 @@
 # the message ID of the original message, instructing the iOS app to poll this server for the actual message contents.
 # This is to prevent the upstream server and Firebase/APNS from being able to read the message.
 #
+# - upstream-base-url is the base URL of the upstream server. Should be "https://ntfy.sh".
+# - upstream-access-token is the token used to authenticate with the upstream server. This is only required
+#   if you exceed the upstream rate limits, or the uptream server requires authentication.
+#
 # upstream-base-url:
+# upstream-access-token:
 
 # Rate limiting: Total number of topics before the server rejects new topics.
 #

+ 62 - 1
server/server_test.go

@@ -18,6 +18,7 @@ import (
 	"runtime/debug"
 	"strings"
 	"sync"
+	"sync/atomic"
 	"testing"
 	"time"
 
@@ -2491,6 +2492,66 @@ func TestServer_PublishWithUTF8MimeHeader(t *testing.T) {
 	require.Equal(t, "ntfy 很棒", m.Tags[1])
 }
 
+func TestServer_UpstreamBaseURL_Success(t *testing.T) {
+	var pollID atomic.Pointer[string]
+	upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		body, err := io.ReadAll(r.Body)
+		require.Nil(t, err)
+		require.Equal(t, "/87c9cddf7b0105f5fe849bf084c6e600be0fde99be3223335199b4965bd7b735", r.URL.Path)
+		require.Equal(t, "", string(body))
+		require.NotEmpty(t, r.Header.Get("X-Poll-ID"))
+		pollID.Store(util.String(r.Header.Get("X-Poll-ID")))
+	}))
+	defer upstreamServer.Close()
+
+	c := newTestConfigWithAuthFile(t)
+	c.BaseURL = "http://myserver.internal"
+	c.UpstreamBaseURL = upstreamServer.URL
+	s := newTestServer(t, c)
+
+	// Send message, and wait for upstream server to receive it
+	response := request(t, s, "PUT", "/mytopic", `hi there`, nil)
+	require.Equal(t, 200, response.Code)
+	m := toMessage(t, response.Body.String())
+	require.NotEmpty(t, m.ID)
+	require.Equal(t, "hi there", m.Message)
+	waitFor(t, func() bool {
+		pID := pollID.Load()
+		return pID != nil && *pID == m.ID
+	})
+}
+
+func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) {
+	var pollID atomic.Pointer[string]
+	upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		body, err := io.ReadAll(r.Body)
+		require.Nil(t, err)
+		require.Equal(t, "/a1c72bcb4daf5af54d13ef86aea8f76c11e8b88320d55f1811d5d7b173bcc1df", r.URL.Path)
+		require.Equal(t, "Bearer tk_1234567890", r.Header.Get("Authorization"))
+		require.Equal(t, "", string(body))
+		require.NotEmpty(t, r.Header.Get("X-Poll-ID"))
+		pollID.Store(util.String(r.Header.Get("X-Poll-ID")))
+	}))
+	defer upstreamServer.Close()
+
+	c := newTestConfigWithAuthFile(t)
+	c.BaseURL = "http://myserver.internal"
+	c.UpstreamBaseURL = upstreamServer.URL
+	c.UpstreamAccessToken = "tk_1234567890"
+	s := newTestServer(t, c)
+
+	// Send message, and wait for upstream server to receive it
+	response := request(t, s, "PUT", "/mytopic1", `hi there`, nil)
+	require.Equal(t, 200, response.Code)
+	m := toMessage(t, response.Body.String())
+	require.NotEmpty(t, m.ID)
+	require.Equal(t, "hi there", m.Message)
+	waitFor(t, func() bool {
+		pID := pollID.Load()
+		return pID != nil && *pID == m.ID
+	})
+}
+
 func newTestConfig(t *testing.T) *Config {
 	conf := NewConfig()
 	conf.BaseURL = "http://127.0.0.1:12345"
@@ -2592,7 +2653,7 @@ func waitForWithMaxWait(t *testing.T, maxWait time.Duration, f func() bool) {
 		if f() {
 			return
 		}
-		time.Sleep(100 * time.Millisecond)
+		time.Sleep(50 * time.Millisecond)
 	}
 	t.Fatalf("Function f did not succeed after %v: %v", maxWait, string(debug.Stack()))
 }

+ 6 - 3
server/server_twilio.go

@@ -87,8 +87,9 @@ func (s *Server) callPhoneInternal(data url.Values) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
+	req.Header.Set("User-Agent", "ntfy/"+s.config.Version)
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		return "", err
@@ -110,8 +111,9 @@ func (s *Server) verifyPhoneNumber(v *visitor, r *http.Request, phoneNumber, cha
 	if err != nil {
 		return err
 	}
-	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
+	req.Header.Set("User-Agent", "ntfy/"+s.config.Version)
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		return err
@@ -135,8 +137,9 @@ func (s *Server) verifyPhoneNumberCheck(v *visitor, r *http.Request, phoneNumber
 	if err != nil {
 		return err
 	}
-	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
+	req.Header.Set("User-Agent", "ntfy/"+s.config.Version)
 	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set("Authorization", util.BasicAuth(s.config.TwilioAccount, s.config.TwilioAuthToken))
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 		return err