Remove web-push-(enabled|duration*), change endpoint, other cosmetic changes

pull/751/head
binwiederhier 2023-06-08 14:30:19 -04:00
parent 4ce6fdcc5a
commit d3ac976d05
17 changed files with 55 additions and 101 deletions

View File

@ -94,13 +94,10 @@ var flagsServe = append(
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}), altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "web-push-enabled", Aliases: []string{"web_push_enabled"}, EnvVars: []string{"NTFY_WEB_PUSH_ENABLED"}, Usage: "enable web push (requires public and private key)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-subscriptions-file", Aliases: []string{"web_push_subscriptions_file"}, EnvVars: []string{"NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE"}, Usage: "file used to store web push subscriptions"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-subscriptions-file", Aliases: []string{"web_push_subscriptions_file"}, EnvVars: []string{"NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE"}, Usage: "file used to store web push subscriptions"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}), altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: server.DefaultWebPushExpiryWarningDuration, Usage: "duration after last update to send a warning notification"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: server.DefaultWebPushExpiryDuration, Usage: "duration after last update to expire subscription"}),
) )
var cmdServe = &cli.Command{ var cmdServe = &cli.Command{
@ -136,12 +133,9 @@ func execServe(c *cli.Context) error {
keyFile := c.String("key-file") keyFile := c.String("key-file")
certFile := c.String("cert-file") certFile := c.String("cert-file")
firebaseKeyFile := c.String("firebase-key-file") firebaseKeyFile := c.String("firebase-key-file")
webPushEnabled := c.Bool("web-push-enabled")
webPushPrivateKey := c.String("web-push-private-key") webPushPrivateKey := c.String("web-push-private-key")
webPushPublicKey := c.String("web-push-public-key") webPushPublicKey := c.String("web-push-public-key")
webPushSubscriptionsFile := c.String("web-push-subscriptions-file") webPushSubscriptionsFile := c.String("web-push-subscriptions-file")
webPushExpiryWarningDuration := c.Duration("web-push-expiry-warning-duration")
webPushExpiryDuration := c.Duration("web-push-expiry-duration")
webPushEmailAddress := c.String("web-push-email-address") webPushEmailAddress := c.String("web-push-email-address")
cacheFile := c.String("cache-file") cacheFile := c.String("cache-file")
cacheDuration := c.Duration("cache-duration") cacheDuration := c.Duration("cache-duration")
@ -197,12 +191,10 @@ func execServe(c *cli.Context) error {
// Check values // Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) { if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
return errors.New("if set, FCM key file must exist") return errors.New("if set, FCM key file must exist")
} else if webPushEnabled && (webPushPrivateKey == "" || webPushPublicKey == "" || webPushSubscriptionsFile == "" || webPushEmailAddress == "" || baseURL == "") { } else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushSubscriptionsFile == "" || webPushEmailAddress == "" || baseURL == "") {
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-subscriptions-file, web-push-email-address, and base-url should be set. run 'ntfy web-push generate-keys' to generate keys") return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-subscriptions-file, web-push-email-address, and base-url should be set. run 'ntfy web-push generate-keys' to generate keys")
} else if keepaliveInterval < 5*time.Second { } else if keepaliveInterval < 5*time.Second {
return errors.New("keepalive interval cannot be lower than five seconds") return errors.New("keepalive interval cannot be lower than five seconds")
} else if webPushEnabled && (webPushExpiryWarningDuration < 24*time.Hour || (webPushExpiryDuration-webPushExpiryWarningDuration < 24*time.Hour)) {
return errors.New("web push expiry warning duration must be at least 1 day, expire duration must be at least 1 day later")
} else if managerInterval < 5*time.Second { } else if managerInterval < 5*time.Second {
return errors.New("manager interval cannot be lower than five seconds") return errors.New("manager interval cannot be lower than five seconds")
} else if cacheDuration > 0 && cacheDuration < managerInterval { } else if cacheDuration > 0 && cacheDuration < managerInterval {
@ -365,13 +357,10 @@ func execServe(c *cli.Context) error {
conf.MetricsListenHTTP = metricsListenHTTP conf.MetricsListenHTTP = metricsListenHTTP
conf.ProfileListenHTTP = profileListenHTTP conf.ProfileListenHTTP = profileListenHTTP
conf.Version = c.App.Version conf.Version = c.App.Version
conf.WebPushEnabled = webPushEnabled
conf.WebPushPrivateKey = webPushPrivateKey conf.WebPushPrivateKey = webPushPrivateKey
conf.WebPushPublicKey = webPushPublicKey conf.WebPushPublicKey = webPushPublicKey
conf.WebPushSubscriptionsFile = webPushSubscriptionsFile conf.WebPushSubscriptionsFile = webPushSubscriptionsFile
conf.WebPushEmailAddress = webPushEmailAddress conf.WebPushEmailAddress = webPushEmailAddress
conf.WebPushExpiryDuration = webPushExpiryDuration
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
// Set up hot-reloading of config // Set up hot-reloading of config
go sigHandlerConfigReload(config) go sigHandlerConfigReload(config)

View File

@ -14,7 +14,7 @@ func init() {
} }
var cmdWebPush = &cli.Command{ var cmdWebPush = &cli.Command{
Name: "web-push", Name: "webpush",
Usage: "Generate keys, in the future manage web push subscriptions", Usage: "Generate keys, in the future manage web push subscriptions",
UsageText: "ntfy web-push [generate-keys]", UsageText: "ntfy web-push [generate-keys]",
Category: categoryServer, Category: categoryServer,
@ -22,7 +22,7 @@ var cmdWebPush = &cli.Command{
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
{ {
Action: generateWebPushKeys, Action: generateWebPushKeys,
Name: "generate-keys", Name: "keys",
Usage: "Generate VAPID keys to enable browser background push notifications", Usage: "Generate VAPID keys to enable browser background push notifications",
UsageText: "ntfy web-push generate-keys", UsageText: "ntfy web-push generate-keys",
Category: categoryServer, Category: categoryServer,
@ -36,28 +36,15 @@ func generateWebPushKeys(c *cli.Context) error {
return err return err
} }
fmt.Fprintf(c.App.ErrWriter, `Keys generated. fmt.Fprintf(c.App.ErrWriter, `Web Push keys generated. Add the following lines to your config file:
VAPID Public Key:
%s
VAPID Private Key:
%s
---
Add the following lines to your config file:
web-push-enabled: true
web-push-public-key: %s web-push-public-key: %s
web-push-private-key: %s web-push-private-key: %s
web-push-subscriptions-file: <filename> web-push-subscriptions-file: /var/cache/ntfy/webpush.db # or similar
web-push-email-address: <email address> web-push-email-address: <email address>
Look at the docs for other methods (e.g. command line flags & environment variables). See https://ntfy.sh/docs/config/#web-push for details.
`, publicKey, privateKey)
You will also need to set a base-url.
`, publicKey, privateKey, publicKey, privateKey)
return nil return nil
} }

View File

@ -10,15 +10,15 @@ import (
func TestCLI_WebPush_GenerateKeys(t *testing.T) { func TestCLI_WebPush_GenerateKeys(t *testing.T) {
app, _, _, stderr := newTestApp() app, _, _, stderr := newTestApp()
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "generate-keys")) require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys"))
require.Contains(t, stderr.String(), "Keys generated.") require.Contains(t, stderr.String(), "Web Push keys generated.")
} }
func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error { func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error {
webPushArgs := []string{ webPushArgs := []string{
"ntfy", "ntfy",
"--log-level=ERROR", "--log-level=ERROR",
"web-push", "webpush",
} }
return app.Run(append(webPushArgs, args...)) return app.Run(append(webPushArgs, args...))
} }

View File

@ -791,9 +791,18 @@ it'll show `New message` as a popup.
## Web Push notifications ## Web Push notifications
[Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030)) [Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030))
is supported, but needs a little configuration to enable it. Since there is no central server (other than the browser's push endpoint), allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed.
you have to configure your own [VAPID](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) keys. These identify the publisher, When enabled, the user can enable **background notifications** for their topics in the wep app under Settings. Once enabled by the
and are used to encrypt the messages before sending them to the push endpoint. user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then
forward it to the browser.
To configure Web Push, you need to generate and configure a [VAPID](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) keypair (via `ntfy webpush keys`),
a database to keep track of the browser's subscriptions, and an admin email address (you):
- `web-push-public-key` is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
- `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
- `web-push-subscriptions-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
- `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com`
Limitations: Limitations:
@ -806,32 +815,17 @@ Limitations:
To configure VAPID keys, first generate them: To configure VAPID keys, first generate them:
```sh ```sh
$ ntfy web-push generate-keys $ ntfy webpush keys
Keys generated. Web Push keys generated.
VAPID Public Key:
AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
VAPID Private Key:
AA2BB1234567890abcdefzxcvbnm1234567890
``` ```
Then copy the generated values into your `server.yml` or use the corresponding environment variables or command line arguments: Then copy the generated values into your `server.yml` or use the corresponding environment variables or command line arguments:
```yaml ```yaml
web-push-enabled: true
web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890 web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890 web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890
web-push-subscriptions-file: /var/cache/ntfy/subscriptions.db web-push-subscriptions-file: /var/cache/ntfy/webpush.db
web-push-email-address: sysadmin@example.com web-push-email-address: sysadmin@example.com
# don't forget to set the required base-url for web push notifications
base-url: https://ntfy.example.com
# you can also set custom expiry and warning durations
# the minimum is 1 day for warning, and 1 day after warning for expiry
web-push-expiry-warning-duration: 168h
web-push-expiry-duration: 192h
``` ```
The `web-push-subscriptions-file` is used to store the push subscriptions. Subscriptions do not ever expire automatically, unless the push The `web-push-subscriptions-file` is used to store the push subscriptions. Subscriptions do not ever expire automatically, unless the push
@ -840,7 +834,7 @@ gateway returns an error (e.g. 410 Gone when a user has unsubscribed).
The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription
file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then. file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then.
Changing your public/private keypair is NOT recommended. Browsers only allow one server identity (public key) per origin, and Changing your public/private keypair is **not recommended**. Browsers only allow one server identity (public key) per origin, and
if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission. if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission.
## Tiers ## Tiers
@ -1340,12 +1334,10 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe | | `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact | | `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |
| `web-push-enabled` | `NTFY_WEB_PUSH_ENABLED` | *boolean* (`true` or `false`) | - | Web Push: Enable/disable (requires private and public key below). | | `web-push-enabled` | `NTFY_WEB_PUSH_ENABLED` | *boolean* (`true` or `false`) | - | Web Push: Enable/disable (requires private and public key below). |
| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy web-push generate-keys` to generate | | `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy webpush generate-keys` to generate |
| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy web-push generate-keys` to generate | | `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy webpush generate-keys` to generate |
| `web-push-subscriptions-file` | `NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE` | *string* | - | Web Push: Subscriptions file | | `web-push-subscriptions-file` | `NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE` | *string* | - | Web Push: Subscriptions file |
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address | | `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
| `web-push-expiry-warning-duration` | `NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION` | *duration* | 1 week | Web Push: Time before expiry warning is sent (min 1 day) |
| `web-push-expiry-duration` | `NTFY_WEB_PUSH_EXPIRY_DURATION` | *duration* | 1 week + 1 day | Web Push: Time before subscription is expired (min 1 day after warning) |
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h. The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k. The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
@ -1443,8 +1435,6 @@ OPTIONS:
--web-push-private-key value, --web_push_private_key value private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY] --web-push-private-key value, --web_push_private_key value private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY]
--web-push-subscriptions-file value, --web_push_subscriptions_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE] --web-push-subscriptions-file value, --web_push_subscriptions_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_SUBSCRIPTIONS_FILE]
--web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS] --web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
--web-push-expiry-warning-duration value, --web_push_expiry_warning_duration value duration after last update to send a warning notification (default: 168h0m0s) [$NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION]
--web-push-expiry-duration value, --web_push_expiry_duration value duration after last update to expire subscription (default: 192h0m0s) [$NTFY_WEB_PUSH_EXPIRY_DURATION]
--help, -h show help --help, -h show help
``` ```

View File

@ -153,7 +153,6 @@ type Config struct {
EnableMetrics bool EnableMetrics bool
AccessControlAllowOrigin string // CORS header field to restrict access from web clients AccessControlAllowOrigin string // CORS header field to restrict access from web clients
Version string // injected by App Version string // injected by App
WebPushEnabled bool
WebPushPrivateKey string WebPushPrivateKey string
WebPushPublicKey string WebPushPublicKey string
WebPushSubscriptionsFile string WebPushSubscriptionsFile string
@ -241,7 +240,6 @@ func NewConfig() *Config {
EnableReservations: false, EnableReservations: false,
AccessControlAllowOrigin: "*", AccessControlAllowOrigin: "*",
Version: "", Version: "",
WebPushEnabled: false,
WebPushPrivateKey: "", WebPushPrivateKey: "",
WebPushPublicKey: "", WebPushPublicKey: "",
WebPushSubscriptionsFile: "", WebPushSubscriptionsFile: "",

View File

@ -94,7 +94,7 @@ var (
apiAccountSettingsPath = "/v1/account/settings" apiAccountSettingsPath = "/v1/account/settings"
apiAccountSubscriptionPath = "/v1/account/subscription" apiAccountSubscriptionPath = "/v1/account/subscription"
apiAccountReservationPath = "/v1/account/reservation" apiAccountReservationPath = "/v1/account/reservation"
apiAccountWebPushPath = "/v1/account/web-push" apiAccountWebPushPath = "/v1/account/webpush"
apiAccountPhonePath = "/v1/account/phone" apiAccountPhonePath = "/v1/account/phone"
apiAccountPhoneVerifyPath = "/v1/account/phone/verify" apiAccountPhoneVerifyPath = "/v1/account/phone/verify"
apiAccountBillingPortalPath = "/v1/account/billing/portal" apiAccountBillingPortalPath = "/v1/account/billing/portal"
@ -157,7 +157,7 @@ func New(conf *Config) (*Server, error) {
return nil, err return nil, err
} }
var webPush *webPushStore var webPush *webPushStore
if conf.WebPushEnabled { if conf.WebPushPublicKey != "" {
webPush, err = newWebPushStore(conf.WebPushSubscriptionsFile) webPush, err = newWebPushStore(conf.WebPushSubscriptionsFile)
if err != nil { if err != nil {
return nil, err return nil, err
@ -574,7 +574,7 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visi
EnableCalls: s.config.TwilioAccount != "", EnableCalls: s.config.TwilioAccount != "",
EnableEmails: s.config.SMTPSenderFrom != "", EnableEmails: s.config.SMTPSenderFrom != "",
EnableReservations: s.config.EnableReservations, EnableReservations: s.config.EnableReservations,
EnableWebPush: s.config.WebPushEnabled, EnableWebPush: s.config.WebPushPublicKey != "",
BillingContact: s.config.BillingContact, BillingContact: s.config.BillingContact,
WebPushPublicKey: s.config.WebPushPublicKey, WebPushPublicKey: s.config.WebPushPublicKey,
DisallowedTopics: s.config.DisallowedTopics, DisallowedTopics: s.config.DisallowedTopics,
@ -792,7 +792,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream
go s.forwardPollRequest(v, m) go s.forwardPollRequest(v, m)
} }
if s.config.WebPushEnabled { if s.config.WebPushPublicKey != "" {
go s.publishToWebPushEndpoints(v, m) go s.publishToWebPushEndpoints(v, m)
} }
} else { } else {
@ -1724,7 +1724,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
if s.config.UpstreamBaseURL != "" { if s.config.UpstreamBaseURL != "" {
go s.forwardPollRequest(v, m) go s.forwardPollRequest(v, m)
} }
if s.config.WebPushEnabled { if s.config.WebPushPublicKey != "" {
go s.publishToWebPushEndpoints(v, m) go s.publishToWebPushEndpoints(v, m)
} }
if err := s.messageCache.MarkPublished(m); err != nil { if err := s.messageCache.MarkPublished(m); err != nil {

View File

@ -40,15 +40,12 @@
# Enable web push # Enable web push
# #
# Run "ntfy web-push generate-keys" to generate the keys # Run "ntfy webpush keys" to generate the keys
# #
# web-push-enabled: false
# web-push-public-key: # web-push-public-key:
# web-push-private-key: # web-push-private-key:
# web-push-subscriptions-file: # web-push-subscriptions-file:
# web-push-email-address: # web-push-email-address:
# web-push-expiry-warning-duration: 168h
# web-push-expiry-duration: 192h
# If "cache-file" is set, messages are cached in a local SQLite database instead of only in-memory. # If "cache-file" is 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. # This allows for service restarts without losing messages in support of the since= parameter.

View File

@ -15,7 +15,7 @@ func (s *Server) execManager() {
s.pruneTokens() s.pruneTokens()
s.pruneAttachments() s.pruneAttachments()
s.pruneMessages() s.pruneMessages()
if s.config.WebPushEnabled { if s.config.WebPushPublicKey != "" {
s.expireOrNotifyOldSubscriptions() s.expireOrNotifyOldSubscriptions()
} }

View File

@ -60,7 +60,7 @@ func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
func (s *Server) ensureWebPushEnabled(next handleFunc) handleFunc { func (s *Server) ensureWebPushEnabled(next handleFunc) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error { return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if !s.config.WebPushEnabled { if s.config.WebPushPublicKey == "" {
return errHTTPNotFound return errHTTPNotFound
} }
return next(w, r, v) return next(w, r, v)

View File

@ -2622,8 +2622,7 @@ func newTestConfigWithWebPush(t *testing.T) *Config {
conf := newTestConfig(t) conf := newTestConfig(t)
privateKey, publicKey, err := webpush.GenerateVAPIDKeys() privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
require.Nil(t, err) require.Nil(t, err)
conf.WebPushEnabled = true conf.WebPushSubscriptionsFile = filepath.Join(t.TempDir(), "webpush.db")
conf.WebPushSubscriptionsFile = filepath.Join(t.TempDir(), "subscriptions.db")
conf.WebPushEmailAddress = "testing@example.com" conf.WebPushEmailAddress = "testing@example.com"
conf.WebPushPrivateKey = privateKey conf.WebPushPrivateKey = privateKey
conf.WebPushPublicKey = publicKey conf.WebPushPublicKey = publicKey

View File

@ -76,7 +76,7 @@ func (s *Server) publishToWebPushEndpoints(v *visitor, m *message) {
} }
// TODO this should return error // TODO this should return error
// TODO the updated_at field is not actually updated ever // TODO rate limiting
func (s *Server) expireOrNotifyOldSubscriptions() { func (s *Server) expireOrNotifyOldSubscriptions() {
subscriptions, err := s.webPush.ExpireAndGetExpiringSubscriptions(s.config.WebPushExpiryWarningDuration, s.config.WebPushExpiryDuration) subscriptions, err := s.webPush.ExpireAndGetExpiringSubscriptions(s.config.WebPushExpiryWarningDuration, s.config.WebPushExpiryDuration)

View File

@ -23,7 +23,7 @@ const (
func TestServer_WebPush_TopicAdd(t *testing.T) { func TestServer_WebPush_TopicAdd(t *testing.T) {
s := newTestServer(t, newTestConfigWithWebPush(t)) s := newTestServer(t, newTestConfigWithWebPush(t))
response := request(t, s, "PUT", "/v1/account/web-push", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), nil) response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), nil)
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
require.Equal(t, `{"success":true}`+"\n", response.Body.String()) require.Equal(t, `{"success":true}`+"\n", response.Body.String())
@ -40,7 +40,7 @@ func TestServer_WebPush_TopicAdd(t *testing.T) {
func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) { func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) {
s := newTestServer(t, newTestConfigWithWebPush(t)) s := newTestServer(t, newTestConfigWithWebPush(t))
response := request(t, s, "PUT", "/v1/account/web-push", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil) response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil)
require.Equal(t, 400, response.Code) require.Equal(t, 400, response.Code)
require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String()) require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String())
} }
@ -53,7 +53,7 @@ func TestServer_WebPush_TopicAdd_TooManyTopics(t *testing.T) {
topicList[i] = util.RandomString(5) topicList[i] = util.RandomString(5)
} }
response := request(t, s, "PUT", "/v1/account/web-push", payloadForTopics(t, topicList, defaultEndpoint), nil) response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, topicList, defaultEndpoint), nil)
require.Equal(t, 400, response.Code) require.Equal(t, 400, response.Code)
require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String()) require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String())
} }
@ -64,7 +64,7 @@ func TestServer_WebPush_TopicUnsubscribe(t *testing.T) {
addSubscription(t, s, "test-topic", defaultEndpoint) addSubscription(t, s, "test-topic", defaultEndpoint)
requireSubscriptionCount(t, s, "test-topic", 1) requireSubscriptionCount(t, s, "test-topic", 1)
response := request(t, s, "PUT", "/v1/account/web-push", payloadForTopics(t, []string{}, defaultEndpoint), nil) response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{}, defaultEndpoint), nil)
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
require.Equal(t, `{"success":true}`+"\n", response.Body.String()) require.Equal(t, `{"success":true}`+"\n", response.Body.String())
@ -79,7 +79,7 @@ func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) {
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
response := request(t, s, "PUT", "/v1/account/web-push", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), map[string]string{ response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), map[string]string{
"Authorization": util.BasicAuth("ben", "ben"), "Authorization": util.BasicAuth("ben", "ben"),
}) })
require.Equal(t, 200, response.Code) require.Equal(t, 200, response.Code)
@ -96,7 +96,7 @@ func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) {
config.AuthDefault = user.PermissionDenyAll config.AuthDefault = user.PermissionDenyAll
s := newTestServer(t, config) s := newTestServer(t, config)
response := request(t, s, "PUT", "/v1/account/web-push", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), nil) response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), nil)
require.Equal(t, 403, response.Code) require.Equal(t, 403, response.Code)
requireSubscriptionCount(t, s, "test-topic", 0) requireSubscriptionCount(t, s, "test-topic", 0)
@ -109,7 +109,7 @@ func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) {
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite)) require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
response := request(t, s, "PUT", "/v1/account/web-push", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), map[string]string{ response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), map[string]string{
"Authorization": util.BasicAuth("ben", "ben"), "Authorization": util.BasicAuth("ben", "ben"),
}) })

View File

@ -31,10 +31,9 @@ const (
INSERT OR REPLACE INTO subscriptions (topic, user_id, endpoint, key_auth, key_p256dh) INSERT OR REPLACE INTO subscriptions (topic, user_id, endpoint, key_auth, key_p256dh)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
` `
deleteWebPushSubscriptionByEndpointQuery = `DELETE FROM subscriptions WHERE endpoint = ?` deleteWebPushSubscriptionByEndpointQuery = `DELETE FROM subscriptions WHERE endpoint = ?`
deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscriptions WHERE user_id = ?` deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscriptions WHERE user_id = ?`
deleteWebPushSubscriptionByTopicAndEndpointQuery = `DELETE FROM subscriptions WHERE topic = ? AND endpoint = ?` deleteWebPushSubscriptionsByAgeQuery = `DELETE FROM subscriptions WHERE warning_sent = 1 AND updated_at <= datetime('now', ?)`
deleteWebPushSubscriptionsByAgeQuery = `DELETE FROM subscriptions WHERE warning_sent = 1 AND updated_at <= datetime('now', ?)`
selectWebPushSubscriptionsForTopicQuery = `SELECT endpoint, key_auth, key_p256dh, user_id FROM subscriptions WHERE topic = ?` selectWebPushSubscriptionsForTopicQuery = `SELECT endpoint, key_auth, key_p256dh, user_id FROM subscriptions WHERE topic = ?`
selectWebPushSubscriptionsExpiringSoonQuery = `SELECT DISTINCT endpoint, key_auth, key_p256dh FROM subscriptions WHERE warning_sent = 0 AND updated_at <= datetime('now', ?)` selectWebPushSubscriptionsExpiringSoonQuery = `SELECT DISTINCT endpoint, key_auth, key_p256dh FROM subscriptions WHERE warning_sent = 0 AND updated_at <= datetime('now', ?)`
@ -169,8 +168,7 @@ func (c *webPushStore) ExpireAndGetExpiringSubscriptions(warningDuration time.Du
return nil, err return nil, err
} }
err = tx.Commit() if err = tx.Commit(); err != nil {
if err != nil {
return nil, err return nil, err
} }

View File

@ -6,7 +6,7 @@ import {
topicUrlAuth, topicUrlAuth,
topicUrlJsonPoll, topicUrlJsonPoll,
topicUrlJsonPollWithSince, topicUrlJsonPollWithSince,
webPushSubscriptionsUrl, accountWebPushUrl,
} from "./utils"; } from "./utils";
import userManager from "./UserManager"; import userManager from "./UserManager";
import { fetchOrThrow } from "./errors"; import { fetchOrThrow } from "./errors";
@ -117,7 +117,7 @@ class Api {
async updateWebPushSubscriptions(topics, browserSubscription) { async updateWebPushSubscriptions(topics, browserSubscription) {
const user = await userManager.get(config.base_url); const user = await userManager.get(config.base_url);
const url = webPushSubscriptionsUrl(config.base_url); const url = accountWebPushUrl(config.base_url);
console.log(`[Api] Sending Web Push Subscriptions`, { url, topics, endpoint: browserSubscription.endpoint }); console.log(`[Api] Sending Web Push Subscriptions`, { url, topics, endpoint: browserSubscription.endpoint });
const response = await fetch(url, { const response = await fetch(url, {

View File

@ -52,17 +52,14 @@ class Notifier {
if (!this.pushPossible()) { if (!this.pushPossible()) {
throw new Error("Unsupported or denied"); throw new Error("Unsupported or denied");
} }
const pushManager = await this.pushManager(); const pushManager = await this.pushManager();
const existingSubscription = await pushManager.getSubscription(); const existingSubscription = await pushManager.getSubscription();
if (existingSubscription) { if (existingSubscription) {
return existingSubscription; return existingSubscription;
} }
// create a new subscription only if web push is enabled // Create a new subscription only if web push is enabled.
// it is possible that web push was previously enabled and then disabled again // It is possible that web push was previously enabled and then disabled again
// in which case there would be an existingSubscription. // in which case there would be an existingSubscription.
// but if it was _not_ enabled previously, we reach here, and only create a new // but if it was _not_ enabled previously, we reach here, and only create a new
// subscription if it is now enabled. // subscription if it is now enabled.

View File

@ -113,7 +113,6 @@ class SubscriptionManager {
async refreshWebPushSubscriptions(presetTopics) { async refreshWebPushSubscriptions(presetTopics) {
const topics = presetTopics ?? (await this.webPushTopics()); const topics = presetTopics ?? (await this.webPushTopics());
const browserSubscription = await notifier.getBrowserSubscription(); const browserSubscription = await notifier.getBrowserSubscription();
if (!browserSubscription) { if (!browserSubscription) {

View File

@ -21,7 +21,6 @@ export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, top
export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`; export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`; export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
export const webPushSubscriptionsUrl = (baseUrl) => `${baseUrl}/v1/account/web-push`;
export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`; export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`;
export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`; export const accountPasswordUrl = (baseUrl) => `${baseUrl}/v1/account/password`;
export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`; export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`;
@ -33,6 +32,7 @@ export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`; export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`; export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`; export const accountPhoneVerifyUrl = (baseUrl) => `${baseUrl}/v1/account/phone/verify`;
export const accountWebPushUrl = (baseUrl) => `${baseUrl}/v1/account/webpush`;
export const validUrl = (url) => url.match(/^https?:\/\/.+/); export const validUrl = (url) => url.match(/^https?:\/\/.+/);