Compare commits
58 Commits
c17aa8bc7e
...
1dc08ee0bd
| Author | SHA1 | Date |
|---|---|---|
|
|
1dc08ee0bd | |
|
|
de8e3bc2aa | |
|
|
d3f7aa7008 | |
|
|
bbfaf2fc4d | |
|
|
db4ac158e3 | |
|
|
7a33e16945 | |
|
|
eac49feb04 | |
|
|
849884c947 | |
|
|
2cb4d089ab | |
|
|
dc797f8594 | |
|
|
061677a78b | |
|
|
b4f15ec9d4 | |
|
|
af17661053 | |
|
|
635ec88c4f | |
|
|
905f048ab4 | |
|
|
7f86108379 | |
|
|
425e6d064e | |
|
|
ebb61fcccf | |
|
|
9f72eb804d | |
|
|
42af71e546 | |
|
|
df818cfebc | |
|
|
0de1990c01 | |
|
|
f40023aa23 | |
|
|
5765a707fc | |
|
|
5eb84f759b | |
|
|
df7dd9c498 | |
|
|
6fe3913aee | |
|
|
0ad9716241 | |
|
|
f4c37ccfb9 | |
|
|
7182d3a4e5 | |
|
|
eecd3245f0 | |
|
|
4dc3b38c95 | |
|
|
9edab24d4c | |
|
|
3b627b27b3 | |
|
|
80462f7ee5 | |
|
|
65e377ec63 | |
|
|
45e1707d3b | |
|
|
0581a9e680 | |
|
|
0fb60ae72d | |
|
|
e36e4856c9 | |
|
|
fa48639517 | |
|
|
2b40ad9a12 | |
|
|
ad7ab18fb7 | |
|
|
8f9dafce20 | |
|
|
7aab7d387f | |
|
|
44b7c2f198 | |
|
|
a49cafbadb | |
|
|
0aee6252bb | |
|
|
0e6a483b2f | |
|
|
20c014ba8d | |
|
|
926967b6e7 | |
|
|
136b656ccb | |
|
|
c844c24a16 | |
|
|
5211d06f2c | |
|
|
e5b2c37c44 | |
|
|
5d2351a13a | |
|
|
3de5a4f2a2 | |
|
|
b6983e6866 |
|
|
@ -90,6 +90,8 @@ nfpms:
|
|||
type: "config|noreplace"
|
||||
- src: client/ntfy-client.service
|
||||
dst: /lib/systemd/system/ntfy-client.service
|
||||
- src: client/user/ntfy-client.service
|
||||
dst: /lib/systemd/user/ntfy-client.service
|
||||
- dst: /var/cache/ntfy
|
||||
type: dir
|
||||
- dst: /var/cache/ntfy/attachments
|
||||
|
|
@ -119,6 +121,7 @@ archives:
|
|||
- server/ntfy.service
|
||||
- client/client.yml
|
||||
- client/ntfy-client.service
|
||||
- client/user/ntfy-client.service
|
||||
-
|
||||
id: ntfy_windows
|
||||
builds:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
|
|||
LABEL org.opencontainers.image.title="ntfy"
|
||||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
RUN apk add --no-cache tzdata \
|
||||
&& /usr/sbin/adduser -D -u 10001 ntfy
|
||||
COPY ntfy /usr/bin
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ LABEL org.opencontainers.image.title="ntfy"
|
|||
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
|
||||
LABEL org.opencontainers.image.version="$VERSION"
|
||||
|
||||
RUN /usr/sbin/adduser -D -u 10001 ntfy
|
||||
|
||||
COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
[Unit]
|
||||
Description=ntfy client
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/ntfy subscribe --config "%h/.config/ntfy/client.yml" --from-config
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
26
cmd/serve.go
26
cmd/serve.go
|
|
@ -88,7 +88,9 @@ var flagsServe = append(
|
|||
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "use specified header to determine visitor IP address (for rate limiting)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-addresses", Aliases: []string{"proxy_trusted_addresses"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_ADDRESSES"}, Value: "", Usage: "comma-separated list of trusted IP addresses to remove from forwarded header"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}),
|
||||
|
|
@ -100,6 +102,8 @@ var flagsServe = append(
|
|||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_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-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "automatically expire unused subscriptions after this time"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}),
|
||||
)
|
||||
|
||||
var cmdServe = &cli.Command{
|
||||
|
|
@ -140,6 +144,8 @@ func execServe(c *cli.Context) error {
|
|||
webPushFile := c.String("web-push-file")
|
||||
webPushEmailAddress := c.String("web-push-email-address")
|
||||
webPushStartupQueries := c.String("web-push-startup-queries")
|
||||
webPushExpiryDurationStr := c.String("web-push-expiry-duration")
|
||||
webPushExpiryWarningDurationStr := c.String("web-push-expiry-warning-duration")
|
||||
cacheFile := c.String("cache-file")
|
||||
cacheDurationStr := c.String("cache-duration")
|
||||
cacheStartupQueries := c.String("cache-startup-queries")
|
||||
|
|
@ -186,6 +192,8 @@ func execServe(c *cli.Context) error {
|
|||
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
|
||||
visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish")
|
||||
behindProxy := c.Bool("behind-proxy")
|
||||
proxyForwardedHeader := c.String("proxy-forwarded-header")
|
||||
proxyTrustedAddresses := util.SplitNoEmpty(c.String("proxy-trusted-addresses"), ",")
|
||||
stripeSecretKey := c.String("stripe-secret-key")
|
||||
stripeWebhookKey := c.String("stripe-webhook-key")
|
||||
billingContact := c.String("billing-contact")
|
||||
|
|
@ -226,6 +234,14 @@ func execServe(c *cli.Context) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("invalid visitor email limit replenish: %s", visitorEmailLimitReplenishStr)
|
||||
}
|
||||
webPushExpiryDuration, err := util.ParseDuration(webPushExpiryDurationStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid web push expiry duration: %s", webPushExpiryDurationStr)
|
||||
}
|
||||
webPushExpiryWarningDuration, err := util.ParseDuration(webPushExpiryWarningDurationStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid web push expiry warning duration: %s", webPushExpiryWarningDurationStr)
|
||||
}
|
||||
|
||||
// Convert sizes to bytes
|
||||
messageSizeLimit, err := util.ParseSize(messageSizeLimitStr)
|
||||
|
|
@ -304,6 +320,10 @@ func execServe(c *cli.Context) error {
|
|||
if messageSizeLimit > 5*1024*1024 {
|
||||
return errors.New("message-size-limit cannot be higher than 5M")
|
||||
}
|
||||
} else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration {
|
||||
return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
|
||||
} else if behindProxy && proxyForwardedHeader == "" {
|
||||
return errors.New("if behind-proxy is set, proxy-forwarded-header must also be set")
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
|
|
@ -402,6 +422,8 @@ func execServe(c *cli.Context) error {
|
|||
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
||||
conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting
|
||||
conf.BehindProxy = behindProxy
|
||||
conf.ProxyForwardedHeader = proxyForwardedHeader
|
||||
conf.ProxyTrustedAddresses = proxyTrustedAddresses
|
||||
conf.StripeSecretKey = stripeSecretKey
|
||||
conf.StripeWebhookKey = stripeWebhookKey
|
||||
conf.BillingContact = billingContact
|
||||
|
|
@ -417,6 +439,8 @@ func execServe(c *cli.Context) error {
|
|||
conf.WebPushFile = webPushFile
|
||||
conf.WebPushEmailAddress = webPushEmailAddress
|
||||
conf.WebPushStartupQueries = webPushStartupQueries
|
||||
conf.WebPushExpiryDuration = webPushExpiryDuration
|
||||
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
|
||||
|
||||
// Set up hot-reloading of config
|
||||
go sigHandlerConfigReload(config)
|
||||
|
|
|
|||
37
cmd/user.go
37
cmd/user.go
|
|
@ -42,7 +42,7 @@ var cmdUser = &cli.Command{
|
|||
Name: "add",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Adds a new user",
|
||||
UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME",
|
||||
UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD_HASH=... ntfy user add [--role=admin|user] USERNAME",
|
||||
Action: execUserAdd,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(user.RoleUser), Usage: "user role"},
|
||||
|
|
@ -55,12 +55,13 @@ granted otherwise by the auth-default-access setting). An admin user has read an
|
|||
topics.
|
||||
|
||||
Examples:
|
||||
ntfy user add phil # Add regular user phil
|
||||
ntfy user add --role=admin phil # Add admin user phil
|
||||
NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts)
|
||||
ntfy user add phil # Add regular user phil
|
||||
ntfy user add --role=admin phil # Add admin user phil
|
||||
NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts)
|
||||
NTFY_PASSWORD_HASH=... ntfy user add phil # Add user, using env variable to set password hash (for scripts)
|
||||
|
||||
You may set the NTFY_PASSWORD environment variable to pass the password. This is useful if
|
||||
you are creating users via scripts.
|
||||
You may set the NTFY_PASSWORD environment variable to pass the password, or NTFY_PASSWORD_HASH to pass
|
||||
directly the bcrypt hash. This is useful if you are creating users via scripts.
|
||||
`,
|
||||
},
|
||||
{
|
||||
|
|
@ -79,7 +80,7 @@ Example:
|
|||
Name: "change-pass",
|
||||
Aliases: []string{"chp"},
|
||||
Usage: "Changes a user's password",
|
||||
UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME",
|
||||
UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME\nNTFY_PASSWORD_HASH=... ntfy user change-pass USERNAME",
|
||||
Action: execUserChangePass,
|
||||
Description: `Change the password for the given user.
|
||||
|
||||
|
|
@ -89,9 +90,10 @@ it twice.
|
|||
Example:
|
||||
ntfy user change-pass phil
|
||||
NTFY_PASSWORD=.. ntfy user change-pass phil
|
||||
NTFY_PASSWORD_HASH=.. ntfy user change-pass phil
|
||||
|
||||
You may set the NTFY_PASSWORD environment variable to pass the new password. This is
|
||||
useful if you are updating users via scripts.
|
||||
You may set the NTFY_PASSWORD environment variable to pass the new password or NTFY_PASSWORD_HASH to pass
|
||||
directly the bcrypt hash. This is useful if you are updating users via scripts.
|
||||
|
||||
`,
|
||||
},
|
||||
|
|
@ -174,7 +176,12 @@ variable to pass the new password. This is useful if you are creating/updating u
|
|||
func execUserAdd(c *cli.Context) error {
|
||||
username := c.Args().Get(0)
|
||||
role := user.Role(c.String("role"))
|
||||
password := os.Getenv("NTFY_PASSWORD")
|
||||
password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH")
|
||||
|
||||
if !hashed {
|
||||
password = os.Getenv("NTFY_PASSWORD")
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
return errors.New("username expected, type 'ntfy user add --help' for help")
|
||||
} else if username == userEveryone || username == user.Everyone {
|
||||
|
|
@ -200,7 +207,7 @@ func execUserAdd(c *cli.Context) error {
|
|||
}
|
||||
password = p
|
||||
}
|
||||
if err := manager.AddUser(username, password, role); err != nil {
|
||||
if err := manager.AddUser(username, password, role, hashed); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "user %s added with role %s\n", username, role)
|
||||
|
|
@ -230,7 +237,11 @@ func execUserDel(c *cli.Context) error {
|
|||
|
||||
func execUserChangePass(c *cli.Context) error {
|
||||
username := c.Args().Get(0)
|
||||
password := os.Getenv("NTFY_PASSWORD")
|
||||
password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH")
|
||||
|
||||
if !hashed {
|
||||
password = os.Getenv("NTFY_PASSWORD")
|
||||
}
|
||||
if username == "" {
|
||||
return errors.New("username expected, type 'ntfy user change-pass --help' for help")
|
||||
} else if username == userEveryone || username == user.Everyone {
|
||||
|
|
@ -249,7 +260,7 @@ func execUserChangePass(c *cli.Context) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if err := manager.ChangePassword(username, password); err != nil {
|
||||
if err := manager.ChangePassword(username, password, hashed); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(c.App.ErrWriter, "changed password for user %s\n", username)
|
||||
|
|
|
|||
|
|
@ -295,7 +295,7 @@ want to use a dedicated token to publish from your backup host, and one from you
|
|||
but not yet implemented.
|
||||
|
||||
The `ntfy token` command can be used to manage access tokens for users. Tokens can have labels, and they can expire
|
||||
automatically (or never expire). Each user can have up to 20 tokens (hardcoded).
|
||||
automatically (or never expire). Each user can have up to 60 tokens (hardcoded).
|
||||
|
||||
**Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details):
|
||||
```
|
||||
|
|
@ -553,17 +553,69 @@ It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache),
|
|||
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.
|
||||
|
||||
### IP-based rate limiting
|
||||
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.
|
||||
[rate limiting](#rate-limiting) logic to use the header configured in `proxy-forwarded-header` (default is `X-Forwarded-For`)
|
||||
as the primary identifier for a visitor, as opposed to the remote IP address.
|
||||
|
||||
=== "/etc/ntfy/server.yml"
|
||||
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.
|
||||
|
||||
Relevant flags to consider:
|
||||
|
||||
* `behind-proxy` makes it so that the real visitor IP address is extracted from the header defined in `proxy-forwarded-header`.
|
||||
Without this, the remote address of the incoming connection is used (default: `false`).
|
||||
* `proxy-forwarded-header` is the header to use to identify visitors (default: `X-Forwarded-For`). It may be a single IP address (e.g. `1.2.3.4`),
|
||||
a comma-separated list of IP addresses (e.g. `1.2.3.4, 5.6.7.8`), or an [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)-style
|
||||
header (e.g. `for=1.2.3.4;by=proxy.example.com, for=5.6.7.8`).
|
||||
* `proxy-trusted-addresses` is a comma-separated list of IP addresses that are removed from the forwarded header
|
||||
to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to
|
||||
the forwarded header (default: empty).
|
||||
|
||||
=== "/etc/ntfy/server.yml (behind a proxy)"
|
||||
``` yaml
|
||||
# Tell ntfy to use "X-Forwarded-For" to identify visitors
|
||||
# Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting
|
||||
#
|
||||
# Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set,
|
||||
# the visitor IP will be 1.2.3.4 (right-most address).
|
||||
#
|
||||
behind-proxy: true
|
||||
```
|
||||
|
||||
=== "/etc/ntfy/server.yml (X-Client-IP header)"
|
||||
``` yaml
|
||||
# Tell ntfy to use "X-Client-IP" header to identify visitors for rate limiting
|
||||
#
|
||||
# Example: If "X-Client-IP: 9.9.9.9" is set,
|
||||
# the visitor IP will be 9.9.9.9.
|
||||
#
|
||||
behind-proxy: true
|
||||
proxy-forwarded-header: "X-Client-IP"
|
||||
```
|
||||
|
||||
=== "/etc/ntfy/server.yml (Forwarded header)"
|
||||
``` yaml
|
||||
# Tell ntfy to use "Forwarded" header (RFC 7239) to identify visitors for rate limiting
|
||||
#
|
||||
# Example: If "Forwarded: for=1.2.3.4;by=proxy.example.com, for=9.9.9.9" is set,
|
||||
# the visitor IP will be 9.9.9.9.
|
||||
#
|
||||
behind-proxy: true
|
||||
proxy-forwarded-header: "Forwarded"
|
||||
```
|
||||
|
||||
=== "/etc/ntfy/server.yml (multiple proxies)"
|
||||
``` yaml
|
||||
# Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting,
|
||||
# and to strip the IP addresses of the proxies 1.2.3.4 and 1.2.3.5
|
||||
#
|
||||
# Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set,
|
||||
# the visitor IP will be 9.9.9.9 (right-most unknown address).
|
||||
#
|
||||
behind-proxy: true
|
||||
proxy-trusted-addresses: "1.2.3.4, 1.2.3.5"
|
||||
```
|
||||
|
||||
### TLS/SSL
|
||||
ntfy supports HTTPS/TLS by setting the `listen-https` [config option](#config-options). However, if you
|
||||
are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself (see below).
|
||||
|
|
@ -877,7 +929,9 @@ a database to keep track of the browser's subscriptions, and an admin email addr
|
|||
- `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
|
||||
- `web-push-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`
|
||||
- `web-push-startup-queries` is an optional list of queries to run on startup`
|
||||
- `web-push-startup-queries` is an optional list of queries to run on startup`
|
||||
- `web-push-expiry-warning-duration` defines the duration after which unused subscriptions are sent a warning (default is `55d`)
|
||||
- `web-push-expiry-duration` defines the duration after which unused subscriptions will expire (default is `60d`)
|
||||
|
||||
Limitations:
|
||||
|
||||
|
|
@ -904,8 +958,8 @@ web-push-file: /var/cache/ntfy/webpush.db
|
|||
web-push-email-address: sysadmin@example.com
|
||||
```
|
||||
|
||||
The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 7 days,
|
||||
and will automatically expire after 9 days (not configurable). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed),
|
||||
The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 55 days,
|
||||
and will automatically expire after 60 days (default). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed),
|
||||
subscriptions are also removed automatically.
|
||||
|
||||
The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription
|
||||
|
|
@ -1380,7 +1434,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||
| `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 |
|
||||
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
|
||||
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
|
||||
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). |
|
||||
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). |
|
||||
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
|
||||
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
|
||||
| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#message-cache) |
|
||||
|
|
@ -1388,7 +1442,9 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||
| `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) |
|
||||
| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). |
|
||||
| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. |
|
||||
| `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. |
|
||||
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) |
|
||||
| `proxy-forwarded-header` | `NTFY_PROXY_FORWARDED_HEADER` | *string* | `X-Forwarded-For` | Use specified header to determine visitor IP address (for rate limiting) |
|
||||
| `proxy-trusted-addresses` | `NTFY_PROXY_TRUSTED_ADDRESSES` | *comma-separated list of IPs* | - | Comma-separated list of trusted IP addresses to remove from forwarded header |
|
||||
| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. |
|
||||
| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. |
|
||||
| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. |
|
||||
|
|
@ -1433,6 +1489,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||
| `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions |
|
||||
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
|
||||
| `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup |
|
||||
| `web-push-expiry-duration` | `NTFY_WEB_PUSH_EXPIRY_DURATION` | *duration* | 60d | Web Push: Duration after which a subscription is considered stale and will be deleted. This is to prevent stale subscriptions. |
|
||||
| `web-push-expiry-warning-duration` | `NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION` | *duration* | 55d | Web Push: Duration after which a warning is sent to subscribers that their subscription will expire soon. This is to prevent stale subscriptions. |
|
||||
| `log-format` | `NTFY_LOG_FORMAT` | *string* | `text` | Defines the output format, can be text or json |
|
||||
| `log-file` | `NTFY_LOG_FILE` | *string* | - | Defines the filename to write logs to. If this is not set, ntfy logs to stderr |
|
||||
| `log-level` | `NTFY_LOG_LEVEL` | *string* | `info` | Defines the default log level, can be one of trace, debug, info, warn or error |
|
||||
|
|
@ -1535,5 +1593,7 @@ OPTIONS:
|
|||
--web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_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-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
|
||||
--web-push-expiry-duration value, --web_push_expiry_duration value automatically expire unused subscriptions after this time (default: "60d") [$NTFY_WEB_PUSH_EXPIRY_DURATION]
|
||||
--web-push-expiry-warning-duration value, --web_push_expiry_warning_duration value send web push warning notification after this time before expiring unused subscriptions (default: "55d") [$NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION]
|
||||
--help, -h show help
|
||||
```
|
||||
|
|
|
|||
|
|
@ -30,37 +30,37 @@ deb/rpm packages.
|
|||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.tar.gz
|
||||
tar zxvf ntfy_2.11.0_linux_amd64.tar.gz
|
||||
sudo cp -a ntfy_2.11.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.tar.gz
|
||||
tar zxvf ntfy_2.12.0_linux_amd64.tar.gz
|
||||
sudo cp -a ntfy_2.12.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.11.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.11.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.12.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.12.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.11.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.11.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.12.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.12.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.11.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.11.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.11.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.12.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.12.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.12.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ Manually installing the .deb file:
|
|||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_amd64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
|
|
@ -118,7 +118,7 @@ Manually installing the .deb file:
|
|||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv6.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv6.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
|
|
@ -126,7 +126,7 @@ Manually installing the .deb file:
|
|||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_armv7.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_armv7.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
|
|
@ -134,7 +134,7 @@ Manually installing the .deb file:
|
|||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_arm64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
|
|
@ -144,28 +144,28 @@ Manually installing the .deb file:
|
|||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.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.11.0/ntfy_2.11.0_linux_armv6.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.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.11.0/ntfy_2.11.0_linux_armv7.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.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.11.0/ntfy_2.11.0_linux_arm64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_linux_arm64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
|
|
@ -195,18 +195,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.11.0/ntfy_2.11.0_darwin_all.tar.gz),
|
||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_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.11.0/ntfy_2.11.0_darwin_all.tar.gz > ntfy_2.11.0_darwin_all.tar.gz
|
||||
tar zxvf ntfy_2.11.0_darwin_all.tar.gz
|
||||
sudo cp -a ntfy_2.11.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_darwin_all.tar.gz > ntfy_2.12.0_darwin_all.tar.gz
|
||||
tar zxvf ntfy_2.12.0_darwin_all.tar.gz
|
||||
sudo cp -a ntfy_2.12.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||
mkdir ~/Library/Application\ Support/ntfy
|
||||
cp ntfy_2.11.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
cp ntfy_2.12.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
ntfy --help
|
||||
```
|
||||
|
||||
|
|
@ -224,7 +224,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.11.0/ntfy_2.11.0_windows_amd64.zip),
|
||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.12.0/ntfy_2.12.0_windows_amd64.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).
|
||||
|
|
|
|||
|
|
@ -40,12 +40,14 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
|||
- [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring
|
||||
- [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - Visual data transformation and automation tool
|
||||
- [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong.
|
||||
- [Miniflux](https://miniflux.app/docs/ntfy.html) - Minimalist and opinionated feed reader
|
||||
- [Beszel](https://beszel.dev/guide/notifications/ntfy) - Server monitoring platform
|
||||
|
||||
## Integration via HTTP/SMTP/etc.
|
||||
|
||||
- [Watchtower](https://containrrr.dev/watchtower/) ⭐ - Automating Docker container base image updates (see [integration example](examples.md#watchtower-shoutrrr))
|
||||
- [Jellyfin](https://jellyfin.org/) ⭐ - The Free Software Media System (see [integration example](examples.md#))
|
||||
- [Overseer](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook))
|
||||
- [Overseerr](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook))
|
||||
- [Tautulli](https://github.com/Tautulli/Tautulli) ⭐ - Monitoring and tracking tool for Plex (integration [via webhook](https://github.com/Tautulli/Tautulli/wiki/Notification-Agents-Guide#webhook))
|
||||
- [Mailrise](https://github.com/YoRyan/mailrise) - An SMTP gateway (integration via [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy))
|
||||
- [Proxmox-Ntfy](https://github.com/qtsone/proxmox-ntfy) - Python script that monitors Proxmox tasks and sends notifications using the Ntfy service.
|
||||
|
|
@ -83,7 +85,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
|||
## CLIs + GUIs
|
||||
|
||||
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
|
||||
- [ntfy Desktop client](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
|
||||
- [ntfy-desktop](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
|
||||
- [ntfy-desktop](https://github.com/Aetherinox/ntfy-desktop) - Desktop client for Windows, Linux, and MacOS with push notifications
|
||||
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
|
||||
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
|
||||
- [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop
|
||||
|
|
@ -170,9 +173,19 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
|||
- [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript)
|
||||
- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell)
|
||||
- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell)
|
||||
- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Currently an Overseerr webhook notification to ntfy helper service.
|
||||
|
||||
## Blog + forum posts
|
||||
|
||||
- [Device notifications via HTTP with ntfy](https://alistairshepherd.uk/writing/ntfy/) - alistairshepherd.uk - 6/2025
|
||||
- [Notifications about (almost) anything with ntfy.sh](https://hamatti.org/posts/notifications-about-almost-anything-with-ntfy-sh/) - hamatti.org - 6/2025
|
||||
- [I set up a self-hosted notification service for everything, and I'll never look back](https://www.xda-developers.com/set-up-self-hosted-notification-service/) ⭐ - xda-developers.com - 5/2025
|
||||
- [How to Set Up Ntfy: Self-Hosted Push Notifications Made Easy](https://www.youtube.com/watch?v=wDJDiAYZ3H0) - youtube.com (sass drew) - 1/2025
|
||||
- [The NTFY is a game-changer FREE solution for IT people](https://www.youtube.com/watch?v=NtlztHT-sRw) - youtube.com (Valters Tech Turf) - 1/2025
|
||||
- [Notify: A Powerful Tool for Real-Time Notifications (ntfy.sh)](https://www.youtube.com/watch?v=XXTTeVfGBz0) - youtube.com (LinuxCloudHacks) - 12/2025
|
||||
- [Push notifications with ntfy and n8n](https://www.youtube.com/watch?v=DKG1R3xYvwQ) - youtube.com (Oskar) - 10/2024
|
||||
- [Setup ntfy for selfhosted notifications with Cloudflare Tunnel](https://medium.com/@svenvanginkel/setup-ntfy-for-selfhosted-notifications-with-cloudflare-tunnel-e342f470177d) - medium.com (Sven van Ginkel) - 10/2024
|
||||
- [Self-Host NTFY - How It Works & Easy Setup Guide](https://www.youtube.com/watch?v=79wHc_jfrJE) ⭐ - youtube.com (Techdox)- 9/2024
|
||||
- [ntfy / Emacs Lisp](https://speechcode.com/blog/ntfy/) - speechcode.com - 3/2024
|
||||
- [Boost Your Productivity with ntfy.sh: The Ultimate Notification Tool for Command-Line Users](https://dev.to/archetypal/boost-your-productivity-with-ntfysh-the-ultimate-notification-tool-for-command-line-users-iil) - dev.to - 3/2024
|
||||
- [Nextcloud Talk (F-Droid version) notifications using ntfy (ntfy.sh)](https://www.youtube.com/watch?v=0a6PpfN5PD8) - youtube.com - 2/2024
|
||||
|
|
|
|||
103
docs/releases.md
103
docs/releases.md
|
|
@ -2,6 +2,66 @@
|
|||
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.12.0
|
||||
Released May 29, 2025
|
||||
|
||||
This is mainly a maintenance release that updates dependencies, though since it's been over a year, there are a few
|
||||
new features and bug fixes as well.
|
||||
|
||||
Thanks to everyone who contributed to this release, and special thanks to [@wunter8](https://github.com/wunter8) for his continued
|
||||
user support in Discord/Matrix/GitHub! You rock, man!
|
||||
|
||||
**Features:**
|
||||
|
||||
* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi))
|
||||
* Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii))
|
||||
* Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus))
|
||||
* Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8))
|
||||
* Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht) for implementing)
|
||||
* You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing)
|
||||
* Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29))
|
||||
* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([#1002](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341))
|
||||
* Upgrade to Vite 6 ([#1342](https://github.com/binwiederhier/ntfy/pull/1342), thanks Dependabot)
|
||||
* Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!)
|
||||
* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy))
|
||||
* Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska))
|
||||
* Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause))
|
||||
* Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt))
|
||||
* WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing)
|
||||
* Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing)
|
||||
* Make sure WebPush subscription topics are actually deleted (no ticket)
|
||||
* Increase the number of access tokens per user to 60 ([#1308](https://github.com/binwiederhier/ntfy/issues/1308))
|
||||
* Allow specifying `cache` and `firebase` via JSON publishing ([#1119](https://github.com/binwiederhier/ntfy/issues/1119)/[#1123](https://github.com/binwiederhier/ntfy/pull/1123), thanks to [@stendler](https://github.com/stendler))
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Lots of new integrations and projects. Amazing!
|
||||
* [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp)
|
||||
* [UptimeObserver](https://uptimeobserver.com)
|
||||
* [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay)
|
||||
* [Monibot](https://monibot.io/)
|
||||
* [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy)
|
||||
* [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage)
|
||||
* [ntfy-run](https://github.com/quantum5/ntfy-run)
|
||||
* [Clipboard IO](https://github.com/jim3692/clipboard-io)
|
||||
* [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp)
|
||||
* [InvaderInformant](https://github.com/patricksthannon/InvaderInformant)
|
||||
* Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice))
|
||||
* Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190))
|
||||
* Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan))
|
||||
* Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode))
|
||||
* Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity))
|
||||
* Lots of other tiny docs updates, thanks to everyone who contributed!
|
||||
|
||||
**Languages**
|
||||
|
||||
* Update new languages from Weblate. Thanks to all the contributors!
|
||||
* Added Tamil (தமிழ்) as a new language to the web app
|
||||
|
||||
### ntfy server v2.11.0
|
||||
Released May 13, 2024
|
||||
|
||||
|
|
@ -1373,50 +1433,11 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||
|
||||
## Not released yet
|
||||
|
||||
### ntfy server v2.12.0 (UNRELEASED)
|
||||
### ntfy server v2.13.0 (UNRELEASED)
|
||||
|
||||
**Features:**
|
||||
|
||||
* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi))
|
||||
* Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii))
|
||||
* Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus))
|
||||
* Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8))
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
|
||||
* Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341))
|
||||
* Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!)
|
||||
* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy))
|
||||
* Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska))
|
||||
* Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause))
|
||||
* Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt))
|
||||
* WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing)
|
||||
* Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing)
|
||||
|
||||
**Documentation:**
|
||||
|
||||
* Lots of new integrations and projects. Amazing!
|
||||
* [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp)
|
||||
* [UptimeObserver](https://uptimeobserver.com)
|
||||
* [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay)
|
||||
* [Monibot](https://monibot.io/)
|
||||
* [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy)
|
||||
* [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage)
|
||||
* [ntfy-run](https://github.com/quantum5/ntfy-run)
|
||||
* [Clipboard IO](https://github.com/jim3692/clipboard-io)
|
||||
* [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp)
|
||||
* [InvaderInformant](https://github.com/patricksthannon/InvaderInformant)
|
||||
* Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice))
|
||||
* Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190))
|
||||
* Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan))
|
||||
* Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode))
|
||||
* Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity))
|
||||
* Lots of other tiny docs updates, tanks to everyone who contributed!
|
||||
|
||||
**Languages**
|
||||
|
||||
* Update new languages from Weblate. Thanks to all the contributors!
|
||||
* Added Tamil (தமிழ்) as a new language to the web app
|
||||
* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-addresses` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha))
|
||||
|
||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,10 @@ Here's an example config file that subscribes to three different topics, executi
|
|||
|
||||
=== "~/.config/ntfy/client.yml (Linux)"
|
||||
```yaml
|
||||
default-host: https://ntfy.sh
|
||||
default-user: phill
|
||||
default-password: mypass
|
||||
|
||||
subscribe:
|
||||
- topic: echo-this
|
||||
command: 'echo "Message received: $message"'
|
||||
|
|
@ -210,9 +214,12 @@ Here's an example config file that subscribes to three different topics, executi
|
|||
fi
|
||||
```
|
||||
|
||||
|
||||
=== "~/Library/Application Support/ntfy/client.yml (macOS)"
|
||||
```yaml
|
||||
default-host: https://ntfy.sh
|
||||
default-user: phill
|
||||
default-password: mypass
|
||||
|
||||
subscribe:
|
||||
- topic: echo-this
|
||||
command: 'echo "Message received: $message"'
|
||||
|
|
@ -226,6 +233,10 @@ Here's an example config file that subscribes to three different topics, executi
|
|||
|
||||
=== "%AppData%\ntfy\client.yml (Windows)"
|
||||
```yaml
|
||||
default-host: https://ntfy.sh
|
||||
default-user: phill
|
||||
default-password: mypass
|
||||
|
||||
subscribe:
|
||||
- topic: echo-this
|
||||
command: 'echo Message received: %message%'
|
||||
|
|
@ -263,43 +274,31 @@ will be used, otherwise, the subscription settings will override the defaults.
|
|||
require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
|
||||
|
||||
### Using the systemd service
|
||||
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
|
||||
to subscribe to multiple topics just like in the example above. The service is automatically installed (but not started)
|
||||
if you install the deb/rpm package. To configure it, simply edit `/etc/ntfy/client.yml` and run `sudo systemctl restart ntfy-client`.
|
||||
You can use the `ntfy-client` systemd services to subscribe to multiple topics just like in the example above.
|
||||
|
||||
!!! info
|
||||
The `ntfy-client.service` runs as user `ntfy`, meaning that typical Linux permission restrictions apply. See below
|
||||
for how to fix this.
|
||||
You have the option of either enabling `ntfy-client` as a **system service** (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
|
||||
or **user service** (see [here](https://github.com/binwiederhier/ntfy/blob/main/client/user/ntfy-client.service)). Neither system service nor user service are enabled or started by default, so you have to do that yourself.
|
||||
|
||||
If the service runs on your personal desktop machine, you may want to override the service user/group (`User=` and `Group=`), and
|
||||
adjust the `DISPLAY` and `DBUS_SESSION_BUS_ADDRESS` environment variables. This will allow you to run commands in your X session
|
||||
as the primary machine user.
|
||||
|
||||
You can either manually override these systemd service entries with `sudo systemctl edit ntfy-client`, and add this
|
||||
(assuming your user is `phil`). Don't forget to run `sudo systemctl daemon-reload` and `sudo systemctl restart ntfy-client`
|
||||
after editing the service file:
|
||||
|
||||
=== "/etc/systemd/system/ntfy-client.service.d/override.conf"
|
||||
```
|
||||
[Service]
|
||||
User=phil
|
||||
Group=phil
|
||||
Environment="DISPLAY=:0" "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
|
||||
```
|
||||
Or you can run the following script that creates this override config for you:
|
||||
**System service:** The `ntfy-client` systemd system service runs as the `ntfy` user. When enabled, it is started at system boot. To configure it as a system
|
||||
service, edit `/etc/ntfy/client.yml` and then enable/start the service (as root), like so:
|
||||
|
||||
```
|
||||
sudo sh -c 'cat > /etc/systemd/system/ntfy-client.service.d/override.conf' <<EOF
|
||||
[Service]
|
||||
User=$USER
|
||||
Group=$USER
|
||||
Environment="DISPLAY=:0" "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus"
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable ntfy-client
|
||||
sudo systemctl restart ntfy-client
|
||||
```
|
||||
|
||||
The system service runs as user `ntfy`, meaning that typical Linux permission restrictions apply. It also means that the system service cannot run commands in your X session as the primary machine user (unlike the user service).
|
||||
|
||||
**User service:** The `ntfy-client` user service is run when the user logs into their desktop environment. To enable/start it, edit `~/.config/ntfy/client.yml` and
|
||||
run the following commands (without sudo!):
|
||||
|
||||
```
|
||||
systemctl --user enable ntfy-client
|
||||
systemctl --user restart ntfy-client
|
||||
```
|
||||
|
||||
Unlike the system service, the user service can interact with the user's desktop environment, and run commands like `notify-send` to display desktop notifications.
|
||||
It can also run commands that require access to the user's home directory, such as `gnome-calculator`.
|
||||
|
||||
### Authentication
|
||||
Depending on whether the server is configured to support [access control](../config.md#access-control), some topics
|
||||
|
|
|
|||
30
go.mod
30
go.mod
|
|
@ -6,7 +6,7 @@ toolchain go1.24.0
|
|||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.18.0 // indirect
|
||||
cloud.google.com/go/storage v1.54.0 // indirect
|
||||
cloud.google.com/go/storage v1.55.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/emersion/go-smtp v0.18.0
|
||||
|
|
@ -21,7 +21,7 @@ require (
|
|||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/time v0.11.0
|
||||
google.golang.org/api v0.234.0
|
||||
google.golang.org/api v0.235.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
|
|
@ -47,21 +47,21 @@ require (
|
|||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 // indirect
|
||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
|
|
@ -73,7 +73,7 @@ require (
|
|||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.64.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
|
|
@ -83,9 +83,9 @@ require (
|
|||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
|
||||
|
|
@ -95,10 +95,10 @@ require (
|
|||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.6 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
67
go.sum
67
go.sum
|
|
@ -18,8 +18,8 @@ cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFs
|
|||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
|
||||
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
|
||||
cloud.google.com/go/storage v1.54.0 h1:Du3XEyliAiftfyW0bwfdppm2MMLdpVAfiIg4T2nAI+0=
|
||||
cloud.google.com/go/storage v1.54.0/go.mod h1:hIi9Boe8cHxTyaeqh7KMMwKg088VblFK46C2x/BWaZE=
|
||||
cloud.google.com/go/storage v1.55.0 h1:NESjdAToN9u1tmhVqhXCaCwYBuvEhZLLv0gBr+2znf0=
|
||||
cloud.google.com/go/storage v1.55.0/go.mod h1:ztSmTTwzsdXe5syLVS0YsbFxXuvEmEyZj7v7zChEmuY=
|
||||
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
|
||||
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
|
||||
firebase.google.com/go/v4 v4.15.2 h1:KJtV4rAfO2CVCp40hBfVk+mqUqg7+jQKx7yOgFDnXBg=
|
||||
|
|
@ -28,14 +28,14 @@ github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
|||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0 h1:VaFXBL0NJpiFBtw4aVJpKHeKULVTcHpD+/G0ibZkcBw=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.28.0/go.mod h1:JXkPazkEc/dZTHzOlzv2vT1DlpWSTbSLmu/1KY6Ly0I=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0 h1:QFgWzcdmJlgEAwJz/zePYVJQxfoJGRtgIqZfIUFg5oQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.52.0/go.mod h1:ayYHuYU7iNcNtEs1K9k6D/Bju7u1VEHMQm5qQ1n3GtM=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.52.0 h1:0l8ynskVvq1dvIn5vJbFMf/a/3TqFpRmCMrruFbzlvk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.52.0/go.mod h1:f/ad5NuHnYz8AOZGuR0cY+l36oSCstdxD73YlIchr6I=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0 h1:wbMd4eG/fOhsCa6+IP8uEDvWF5vl7rNoUWmP5f72Tbs=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.52.0/go.mod h1:gdIm9TxRk5soClCwuB0FtdXsbqtw0aqPwBEurK9tPkw=
|
||||
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||
github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s=
|
||||
|
|
@ -51,8 +51,8 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1Ig
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
|
||||
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
|
|
@ -73,8 +73,8 @@ github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFA
|
|||
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
|
||||
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
|
|
@ -124,8 +124,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
|
|
@ -157,16 +158,16 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
|||
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
|
|
@ -258,18 +259,18 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
|
|||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.234.0 h1:d3sAmYq3E9gdr2mpmiWGbm9pHsA/KJmyiLkwKfHBqU4=
|
||||
google.golang.org/api v0.234.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
|
||||
google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo=
|
||||
google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
|
||||
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
||||
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||
google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237 h1:2zGWyk04EwQ3mmV4dd4M4U7P/igHi5p7CBJEg1rI6A8=
|
||||
google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237/go.mod h1:LhI4bRmX3rqllzQ+BGneexULkEjBf2gsAfkbeCA8IbU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/genproto v0.0.0-20250528174236-200df99c418a h1:KXuwdBmgjb4T3l4ZzXhP6HxxFKXD9FcK5/8qfJI4WwU=
|
||||
google.golang.org/genproto v0.0.0-20250528174236-200df99c418a/go.mod h1:Nlk93rrS2X7rV8hiC2gh2A/AJspZhElz9Oh2KGsjLEY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ if [ "$1" = "configure" ] || [ "$1" -ge 1 ]; then
|
|||
fi
|
||||
fi
|
||||
if systemctl is-active -q ntfy-client.service; then
|
||||
echo "Restarting ntfy-client.service ..."
|
||||
echo "Restarting ntfy-client.service (system) ..."
|
||||
if [ -x /usr/bin/deb-systemd-invoke ]; then
|
||||
deb-systemd-invoke try-restart ntfy-client.service >/dev/null || true
|
||||
else
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ const (
|
|||
|
||||
// Defines default Web Push settings
|
||||
const (
|
||||
DefaultWebPushExpiryWarningDuration = 7 * 24 * time.Hour
|
||||
DefaultWebPushExpiryDuration = 9 * 24 * time.Hour
|
||||
DefaultWebPushExpiryWarningDuration = 55 * 24 * time.Hour
|
||||
DefaultWebPushExpiryDuration = 60 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// Defines all global and per-visitor limits
|
||||
|
|
@ -143,7 +143,9 @@ type Config struct {
|
|||
VisitorAuthFailureLimitReplenish time.Duration
|
||||
VisitorStatsResetTime time.Time // Time of the day at which to reset visitor stats
|
||||
VisitorSubscriberRateLimiting bool // Enable subscriber-based rate limiting for UnifiedPush topics
|
||||
BehindProxy bool
|
||||
BehindProxy bool // If true, the server will trust the proxy client IP header to determine the client IP address
|
||||
ProxyForwardedHeader string // The header field to read the real/client IP address from, if BehindProxy is true, defaults to "X-Forwarded-For"
|
||||
ProxyTrustedAddresses []string // List of trusted proxy addresses that will be stripped from the Forwarded header if BehindProxy is true
|
||||
StripeSecretKey string
|
||||
StripeWebhookKey string
|
||||
StripePriceCacheDuration time.Duration
|
||||
|
|
@ -232,7 +234,8 @@ func NewConfig() *Config {
|
|||
VisitorAuthFailureLimitReplenish: DefaultVisitorAuthFailureLimitReplenish,
|
||||
VisitorStatsResetTime: DefaultVisitorStatsResetTime,
|
||||
VisitorSubscriberRateLimiting: false,
|
||||
BehindProxy: false,
|
||||
BehindProxy: false, // If true, the server will trust the proxy client IP header to determine the client IP address
|
||||
ProxyForwardedHeader: "X-Forwarded-For", // Default header for reverse proxy client IPs
|
||||
StripeSecretKey: "",
|
||||
StripeWebhookKey: "",
|
||||
StripePriceCacheDuration: DefaultStripePriceCacheDuration,
|
||||
|
|
|
|||
|
|
@ -446,8 +446,10 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||
return s.ensureWebPushEnabled(s.handleWebManifest)(w, r, v)
|
||||
} else if r.Method == http.MethodGet && r.URL.Path == apiUsersPath {
|
||||
return s.ensureAdmin(s.handleUsersGet)(w, r, v)
|
||||
} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
|
||||
} else if r.Method == http.MethodPost && r.URL.Path == apiUsersPath {
|
||||
return s.ensureAdmin(s.handleUsersAdd)(w, r, v)
|
||||
} else if r.Method == http.MethodPut && r.URL.Path == apiUsersPath {
|
||||
return s.ensureAdmin(s.handleUsersUpdate)(w, r, v)
|
||||
} else if r.Method == http.MethodDelete && r.URL.Path == apiUsersPath {
|
||||
return s.ensureAdmin(s.handleUsersDelete)(w, r, v)
|
||||
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == apiUsersAccessPath {
|
||||
|
|
@ -1866,6 +1868,12 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
|||
if m.Call != "" {
|
||||
r.Header.Set("X-Call", m.Call)
|
||||
}
|
||||
if m.Cache != "" {
|
||||
r.Header.Set("X-Cache", m.Cache)
|
||||
}
|
||||
if m.Firebase != "" {
|
||||
r.Header.Set("X-Firebase", m.Firebase)
|
||||
}
|
||||
return next(w, r, v)
|
||||
}
|
||||
}
|
||||
|
|
@ -1928,8 +1936,8 @@ func (s *Server) authorizeTopic(next handleFunc, perm user.Permission) handleFun
|
|||
// This function will ALWAYS return a visitor, even if an error occurs (e.g. unauthorized), so
|
||||
// that subsequent logging calls still have a visitor context.
|
||||
func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) {
|
||||
// Read "Authorization" header value, and exit out early if it's not set
|
||||
ip := extractIPAddress(r, s.config.BehindProxy)
|
||||
// Read the "Authorization" header value and exit out early if it's not set
|
||||
ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddresses)
|
||||
vip := s.visitor(ip, nil)
|
||||
if s.userManager == nil {
|
||||
return vip, nil
|
||||
|
|
@ -2004,7 +2012,7 @@ func (s *Server) authenticateBearerAuth(r *http.Request, token string) (*user.Us
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ip := extractIPAddress(r, s.config.BehindProxy)
|
||||
ip := extractIPAddress(r, s.config.BehindProxy, s.config.ProxyForwardedHeader, s.config.ProxyTrustedAddresses)
|
||||
go s.userManager.EnqueueTokenUpdate(token, &user.TokenUpdate{
|
||||
LastAccess: time.Now(),
|
||||
LastOrigin: ip,
|
||||
|
|
|
|||
|
|
@ -95,13 +95,23 @@
|
|||
# auth-default-access: "read-write"
|
||||
# auth-startup-queries:
|
||||
|
||||
# If set, the X-Forwarded-For header is used to determine the visitor IP address
|
||||
# instead of the remote address of the connection.
|
||||
# If set, the X-Forwarded-For header (or whatever is configured in proxy-forwarded-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
|
||||
# WARNING: If you are behind a proxy, you must set this, otherwise all visitors are rate-limited
|
||||
# as if they are one.
|
||||
#
|
||||
# - behind-proxy makes it so that the real visitor IP address is extracted from the header defined in
|
||||
# proxy-forwarded-header. Without this, the remote address of the incoming connection is used.
|
||||
# - proxy-forwarded-header is the header to use to identify visitors. It may be a single IP address (e.g. 1.2.3.4),
|
||||
# a comma-separated list of IP addresses (e.g. "1.2.3.4, 5.6.7.8"), or an RFC 7239-style header (e.g. "for=1.2.3.4;by=proxy.example.com, for=5.6.7.8").
|
||||
# - proxy-trusted-addresses is a comma-separated list of IP addresses that are removed from the forwarded header
|
||||
# to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to
|
||||
# the forwarded header.
|
||||
#
|
||||
# behind-proxy: false
|
||||
# proxy-forwarded-header: "X-Forwarded-For"
|
||||
# proxy-trusted-addresses:
|
||||
|
||||
# If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments
|
||||
# are "attachment-cache-dir" and "base-url".
|
||||
|
|
@ -138,7 +148,7 @@
|
|||
# - smtp-server-domain is the e-mail domain, e.g. ntfy.sh
|
||||
# - smtp-server-addr-prefix is an optional prefix for the e-mail addresses to prevent spam. If set to "ntfy-",
|
||||
# for instance, only e-mails to ntfy-$topic@ntfy.sh will be accepted. If this is not set, all emails to
|
||||
# $topic@ntfy.sh will be accepted (which may obviously be a spam problem).
|
||||
# $topic@ntfy.sh will be accepted (which may be a spam problem).
|
||||
#
|
||||
# smtp-server-listen:
|
||||
# smtp-server-domain:
|
||||
|
|
@ -146,7 +156,7 @@
|
|||
|
||||
# Web Push support (background notifications for browsers)
|
||||
#
|
||||
# If enabled, allows ntfy to receive push notifications, even when the ntfy web app is closed. When enabled, users
|
||||
# If enabled, allows the ntfy web app to receive push notifications, even when the web app is closed. When enabled, users
|
||||
# can enable background notifications in the web app. Once enabled, ntfy will forward published messages to the push
|
||||
# endpoint, which will then forward it to the browser.
|
||||
#
|
||||
|
|
@ -155,15 +165,19 @@
|
|||
#
|
||||
# - 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-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`
|
||||
# - web-push-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
|
||||
# - web-push-startup-queries is an optional list of queries to run on startup`
|
||||
# - web-push-expiry-warning-duration defines the duration after which unused subscriptions are sent a warning (default is 55d`)
|
||||
# - web-push-expiry-duration defines the duration after which unused subscriptions will expire (default is 60d)
|
||||
#
|
||||
# web-push-public-key:
|
||||
# web-push-private-key:
|
||||
# web-push-file:
|
||||
# web-push-email-address:
|
||||
# web-push-startup-queries:
|
||||
# web-push-expiry-warning-duration: "55d"
|
||||
# web-push-expiry-duration: "60d"
|
||||
|
||||
# If enabled, ntfy can perform voice calls via Twilio via the "X-Call" header.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
|
|||
return errHTTPConflictUserExists
|
||||
}
|
||||
logvr(v, r).Tag(tagAccount).Field("user_name", newAccount.Username).Info("Creating user %s", newAccount.Username)
|
||||
if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil {
|
||||
if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser, false); err != nil {
|
||||
if errors.Is(err, user.ErrInvalidArgument) {
|
||||
return errHTTPBadRequestInvalidUsername
|
||||
}
|
||||
|
|
@ -207,7 +207,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ
|
|||
return errHTTPBadRequestIncorrectPasswordConfirmation
|
||||
}
|
||||
logvr(v, r).Tag(tagAccount).Debug("Changing password for user %s", u.Name)
|
||||
if err := s.userManager.ChangePassword(u.Name, req.NewPassword); err != nil {
|
||||
if err := s.userManager.ChangePassword(u.Name, req.NewPassword, false); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writeJSON(w, newSuccessResponse())
|
||||
|
|
|
|||
|
|
@ -87,9 +87,9 @@ func TestAccount_Signup_AsUser(t *testing.T) {
|
|||
defer s.closeDatabases()
|
||||
|
||||
log.Info("1")
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
log.Info("2")
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
log.Info("3")
|
||||
rr := request(t, s, "POST", "/v1/account", `{"username":"emma", "password":"emma"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
|
|
@ -174,7 +174,7 @@ func TestAccount_ChangeSettings(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
u, _ := s.userManager.User("phil")
|
||||
token, _ := s.userManager.CreateToken(u.ID, "", time.Unix(0, 0), netip.IPv4Unspecified())
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ func TestAccount_Subscription_AddUpdateDelete(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
rr := request(t, s, "POST", "/v1/account/subscription", `{"base_url": "http://abc.com", "topic": "def"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
|
|
@ -254,7 +254,7 @@ func TestAccount_ChangePassword(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
rr := request(t, s, "POST", "/v1/account/password", `{"password": "WRONG", "new_password": ""}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
|
|
@ -296,7 +296,7 @@ func TestAccount_ExtendToken(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
|
|
@ -332,7 +332,7 @@ func TestAccount_ExtendToken_NoTokenProvided(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
rr := request(t, s, "PATCH", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"), // Not Bearer!
|
||||
|
|
@ -345,7 +345,7 @@ func TestAccount_DeleteToken(t *testing.T) {
|
|||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
rr := request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
|
|
@ -455,14 +455,14 @@ func TestAccount_Reservation_AddAdminSuccess(t *testing.T) {
|
|||
Code: "pro",
|
||||
ReservationLimit: 2,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("noadmin1", "pass", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("noadmin1", "pro"))
|
||||
require.Nil(t, s.userManager.AddReservation("noadmin1", "mytopic", user.PermissionDenyAll))
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("noadmin2", "pass", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("noadmin2", "pro"))
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "adminpass", user.RoleAdmin, false))
|
||||
|
||||
// Admin can reserve topic
|
||||
rr := request(t, s, "POST", "/v1/account/reservation", `{"topic":"sometopic","everyone":"deny-all"}`, map[string]string{
|
||||
|
|
@ -624,7 +624,7 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
|
|||
s := newTestServer(t, conf)
|
||||
|
||||
// Create user with tier
|
||||
require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "mypass", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "pro",
|
||||
MessageLimit: 20,
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ func (s *Server) handleUsersGet(w http.ResponseWriter, r *http.Request, v *visit
|
|||
}
|
||||
|
||||
func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
req, err := readJSONWithLimit[apiUserAddRequest](r.Body, jsonBodyBytesLimit, false)
|
||||
req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !user.AllowedUsername(req.Username) || req.Password == "" {
|
||||
return errHTTPBadRequest.Wrap("username invalid, or password missing")
|
||||
} else if !user.AllowedUsername(req.Username) || (req.Password == "" && req.Hash == "") {
|
||||
return errHTTPBadRequest.Wrap("username invalid, or password/password_hash missing")
|
||||
}
|
||||
u, err := s.userManager.User(req.Username)
|
||||
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
||||
|
|
@ -60,7 +60,11 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
|
|||
return err
|
||||
}
|
||||
}
|
||||
if err := s.userManager.AddUser(req.Username, req.Password, user.RoleUser); err != nil {
|
||||
password, hashed := req.Password, false
|
||||
if req.Hash != "" {
|
||||
password, hashed = req.Hash, true
|
||||
}
|
||||
if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil {
|
||||
return err
|
||||
}
|
||||
if tier != nil {
|
||||
|
|
@ -71,6 +75,53 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
|
|||
return s.writeJSON(w, newSuccessResponse())
|
||||
}
|
||||
|
||||
func (s *Server) handleUsersUpdate(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
req, err := readJSONWithLimit[apiUserAddOrUpdateRequest](r.Body, jsonBodyBytesLimit, false)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !user.AllowedUsername(req.Username) {
|
||||
return errHTTPBadRequest.Wrap("username invalid")
|
||||
} else if req.Password == "" && req.Hash == "" && req.Tier == "" {
|
||||
return errHTTPBadRequest.Wrap("need to provide at least one of \"password\", \"password_hash\" or \"tier\"")
|
||||
}
|
||||
u, err := s.userManager.User(req.Username)
|
||||
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
||||
return err
|
||||
} else if u != nil {
|
||||
if u.IsAdmin() {
|
||||
return errHTTPForbidden
|
||||
}
|
||||
if req.Hash != "" {
|
||||
if err := s.userManager.ChangePassword(req.Username, req.Hash, true); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if req.Password != "" {
|
||||
if err := s.userManager.ChangePassword(req.Username, req.Password, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
password, hashed := req.Password, false
|
||||
if req.Hash != "" {
|
||||
password, hashed = req.Hash, true
|
||||
}
|
||||
if err := s.userManager.AddUser(req.Username, password, user.RoleUser, hashed); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.Tier != "" {
|
||||
if _, err = s.userManager.Tier(req.Tier); errors.Is(err, user.ErrTierNotFound) {
|
||||
return errHTTPBadRequestTierInvalid
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.userManager.ChangeTier(req.Username, req.Tier); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.writeJSON(w, newSuccessResponse())
|
||||
}
|
||||
|
||||
func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
req, err := readJSONWithLimit[apiUserDeleteRequest](r.Body, jsonBodyBytesLimit, false)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ func TestUser_AddRemove(t *testing.T) {
|
|||
defer s.closeDatabases()
|
||||
|
||||
// Create admin, tier
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "tier1",
|
||||
}))
|
||||
|
||||
// Create user via API
|
||||
rr := request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{
|
||||
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
|
@ -49,6 +49,226 @@ func TestUser_AddRemove(t *testing.T) {
|
|||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Check user was deleted
|
||||
users, err = s.userManager.Users()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 3, len(users))
|
||||
require.Equal(t, "phil", users[0].Name)
|
||||
require.Equal(t, "emma", users[1].Name)
|
||||
require.Equal(t, user.Everyone, users[2].Name)
|
||||
|
||||
// Reject invalid user change
|
||||
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 400, rr.Code)
|
||||
}
|
||||
|
||||
func TestUser_AddWithPasswordHash(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
// Create admin
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
|
||||
// Create user via API
|
||||
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Check that user can login with password
|
||||
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Check users
|
||||
users, err := s.userManager.Users()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 3, len(users))
|
||||
require.Equal(t, "phil", users[0].Name)
|
||||
require.Equal(t, user.RoleAdmin, users[0].Role)
|
||||
require.Equal(t, "ben", users[1].Name)
|
||||
require.Equal(t, user.RoleUser, users[1].Role)
|
||||
}
|
||||
|
||||
func TestUser_ChangeUserPassword(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
// Create admin
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
|
||||
// Create user via API
|
||||
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password": "ben"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Try to login with first password
|
||||
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Change password via API
|
||||
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password": "ben-two"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Make sure first password fails
|
||||
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
})
|
||||
require.Equal(t, 401, rr.Code)
|
||||
|
||||
// Try to login with second password
|
||||
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben-two"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
}
|
||||
|
||||
func TestUser_ChangeUserTier(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
// Create admin, tier
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "tier1",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "tier2",
|
||||
}))
|
||||
|
||||
// Create user with tier via API
|
||||
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Check users
|
||||
users, err := s.userManager.Users()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 3, len(users))
|
||||
require.Equal(t, "phil", users[0].Name)
|
||||
require.Equal(t, "ben", users[1].Name)
|
||||
require.Equal(t, user.RoleUser, users[1].Role)
|
||||
require.Equal(t, "tier1", users[1].Tier.Code)
|
||||
|
||||
// Change user tier via API
|
||||
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "tier": "tier2"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Check users again
|
||||
users, err = s.userManager.Users()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "tier2", users[1].Tier.Code)
|
||||
}
|
||||
|
||||
func TestUser_ChangeUserPasswordAndTier(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
// Create admin, tier
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "tier1",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "tier2",
|
||||
}))
|
||||
|
||||
// Create user with tier via API
|
||||
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"ben", "tier": "tier1"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Check users
|
||||
users, err := s.userManager.Users()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 3, len(users))
|
||||
require.Equal(t, "phil", users[0].Name)
|
||||
require.Equal(t, "ben", users[1].Name)
|
||||
require.Equal(t, user.RoleUser, users[1].Role)
|
||||
require.Equal(t, "tier1", users[1].Tier.Code)
|
||||
|
||||
// Change user password and tier via API
|
||||
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "password":"ben-two", "tier": "tier2"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Make sure first password fails
|
||||
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
})
|
||||
require.Equal(t, 401, rr.Code)
|
||||
|
||||
// Try to login with second password
|
||||
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben-two"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Check new tier
|
||||
users, err = s.userManager.Users()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "tier2", users[1].Tier.Code)
|
||||
}
|
||||
|
||||
func TestUser_ChangeUserPasswordWithHash(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
// Create admin
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
|
||||
// Create user with tier via API
|
||||
rr := request(t, s, "POST", "/v1/users", `{"username": "ben", "password":"not-ben"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Try to login with first password
|
||||
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "not-ben"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Change user password and tier via API
|
||||
rr = request(t, s, "PUT", "/v1/users", `{"username": "ben", "hash":"$2a$04$2aPIIqPXQU16OfkSUZH1XOzpu1gsPRKkrfVdFLgWQ.tqb.vtTCuVe"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
||||
// Try to login with second password
|
||||
rr = request(t, s, "POST", "/v1/account/token", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
}
|
||||
|
||||
func TestUser_DontChangeAdminPassword(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
defer s.closeDatabases()
|
||||
|
||||
// Create admin
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
require.Nil(t, s.userManager.AddUser("admin", "admin", user.RoleAdmin, false))
|
||||
|
||||
// Try to change password via API
|
||||
rr := request(t, s, "PUT", "/v1/users", `{"username": "admin", "password": "admin-new"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 403, rr.Code)
|
||||
}
|
||||
|
||||
func TestUser_AddRemove_Failures(t *testing.T) {
|
||||
|
|
@ -56,23 +276,23 @@ func TestUser_AddRemove_Failures(t *testing.T) {
|
|||
defer s.closeDatabases()
|
||||
|
||||
// Create admin
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
|
||||
// Cannot create user with invalid username
|
||||
rr := request(t, s, "PUT", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{
|
||||
rr := request(t, s, "POST", "/v1/users", `{"username": "not valid", "password":"ben"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 400, rr.Code)
|
||||
|
||||
// Cannot create user if user already exists
|
||||
rr = request(t, s, "PUT", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{
|
||||
rr = request(t, s, "POST", "/v1/users", `{"username": "phil", "password":"phil"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 40901, toHTTPError(t, rr.Body.String()).Code)
|
||||
|
||||
// Cannot create user with invalid tier
|
||||
rr = request(t, s, "PUT", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{
|
||||
rr = request(t, s, "POST", "/v1/users", `{"username": "emma", "password":"emma", "tier": "invalid"}`, map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
})
|
||||
require.Equal(t, 40030, toHTTPError(t, rr.Body.String()).Code)
|
||||
|
|
@ -97,8 +317,8 @@ func TestAccess_AllowReset(t *testing.T) {
|
|||
defer s.closeDatabases()
|
||||
|
||||
// User and admin
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
|
||||
// Subscribing not allowed
|
||||
rr := request(t, s, "GET", "/gold/json?poll=1", "", map[string]string{
|
||||
|
|
@ -138,7 +358,7 @@ func TestAccess_AllowReset_NonAdminAttempt(t *testing.T) {
|
|||
defer s.closeDatabases()
|
||||
|
||||
// User
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
|
||||
// Grant access fails, because non-admin
|
||||
rr := request(t, s, "POST", "/v1/users/access", `{"username": "ben", "topic":"gold", "permission":"ro"}`, map[string]string{
|
||||
|
|
@ -154,8 +374,8 @@ func TestAccess_AllowReset_KillConnection(t *testing.T) {
|
|||
defer s.closeDatabases()
|
||||
|
||||
// User and admin, grant access to "gol*" topics
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "gol*", user.PermissionRead)) // Wildcard!
|
||||
|
||||
start, timeTaken := time.Now(), atomic.Int64{}
|
||||
|
|
|
|||
|
|
@ -138,41 +138,7 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro
|
|||
}
|
||||
apnsConfig = createAPNSAlertConfig(m, data)
|
||||
case messageEvent:
|
||||
allowForward := true
|
||||
if auther != nil {
|
||||
allowForward = auther.Authorize(nil, m.Topic, user.PermissionRead) == nil
|
||||
}
|
||||
if allowForward {
|
||||
data = map[string]string{
|
||||
"id": m.ID,
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": m.Event,
|
||||
"topic": m.Topic,
|
||||
"priority": fmt.Sprintf("%d", m.Priority),
|
||||
"tags": strings.Join(m.Tags, ","),
|
||||
"click": m.Click,
|
||||
"icon": m.Icon,
|
||||
"title": m.Title,
|
||||
"message": m.Message,
|
||||
"content_type": m.ContentType,
|
||||
"encoding": m.Encoding,
|
||||
}
|
||||
if len(m.Actions) > 0 {
|
||||
actions, err := json.Marshal(m.Actions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data["actions"] = string(actions)
|
||||
}
|
||||
if m.Attachment != nil {
|
||||
data["attachment_name"] = m.Attachment.Name
|
||||
data["attachment_type"] = m.Attachment.Type
|
||||
data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size)
|
||||
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
|
||||
data["attachment_url"] = m.Attachment.URL
|
||||
}
|
||||
apnsConfig = createAPNSAlertConfig(m, data)
|
||||
} else {
|
||||
// If "anonymous read" for a topic is not allowed, we cannot send the message along
|
||||
// via Firebase. Instead, we send a "poll_request" message, asking the client to poll.
|
||||
//
|
||||
|
|
@ -180,23 +146,42 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro
|
|||
// fields are set, the iOS app fails to decode the message.
|
||||
//
|
||||
// See https://github.com/binwiederhier/ntfy/pull/1345
|
||||
data = map[string]string{
|
||||
"id": m.ID,
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": pollRequestEvent,
|
||||
"topic": m.Topic,
|
||||
"priority": fmt.Sprintf("%d", m.Priority),
|
||||
"tags": "",
|
||||
"click": "",
|
||||
"icon": "",
|
||||
"title": "",
|
||||
"message": newMessageBody,
|
||||
"content_type": m.ContentType,
|
||||
"encoding": m.Encoding,
|
||||
"poll_id": m.ID,
|
||||
if err := auther.Authorize(nil, m.Topic, user.PermissionRead); err != nil {
|
||||
m = toPollRequest(m)
|
||||
}
|
||||
apnsConfig = createAPNSAlertConfig(m, data)
|
||||
}
|
||||
data = map[string]string{
|
||||
"id": m.ID,
|
||||
"time": fmt.Sprintf("%d", m.Time),
|
||||
"event": m.Event,
|
||||
"topic": m.Topic,
|
||||
"priority": fmt.Sprintf("%d", m.Priority),
|
||||
"tags": strings.Join(m.Tags, ","),
|
||||
"click": m.Click,
|
||||
"icon": m.Icon,
|
||||
"title": m.Title,
|
||||
"message": m.Message,
|
||||
"content_type": m.ContentType,
|
||||
"encoding": m.Encoding,
|
||||
}
|
||||
if len(m.Actions) > 0 {
|
||||
actions, err := json.Marshal(m.Actions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data["actions"] = string(actions)
|
||||
}
|
||||
if m.Attachment != nil {
|
||||
data["attachment_name"] = m.Attachment.Name
|
||||
data["attachment_type"] = m.Attachment.Type
|
||||
data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size)
|
||||
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
|
||||
data["attachment_url"] = m.Attachment.URL
|
||||
}
|
||||
if m.PollID != "" {
|
||||
data["poll_id"] = m.PollID
|
||||
}
|
||||
apnsConfig = createAPNSAlertConfig(m, data)
|
||||
}
|
||||
var androidConfig *messaging.AndroidConfig
|
||||
if m.Priority >= 4 {
|
||||
|
|
@ -290,3 +275,17 @@ func maybeTruncateAPNSBodyMessage(s string) string {
|
|||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// toPollRequest converts a message to a poll request message.
|
||||
//
|
||||
// This empties all the fields that are not needed for a poll request and just sets the required fields,
|
||||
// most importantly, the PollID.
|
||||
func toPollRequest(m *message) *message {
|
||||
pr := newPollRequestMessage(m.Topic, m.ID)
|
||||
pr.ID = m.ID
|
||||
pr.Time = m.Time
|
||||
pr.Priority = m.Priority // Keep priority
|
||||
pr.ContentType = m.ContentType
|
||||
pr.Encoding = m.Encoding
|
||||
return pr
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,6 +240,8 @@ func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) {
|
|||
"content_type": "",
|
||||
"poll_id": m.ID,
|
||||
}, fbm.Data)
|
||||
require.Equal(t, "", fbm.APNS.Payload.Aps.Alert.Title)
|
||||
require.Equal(t, "New message", fbm.APNS.Payload.Aps.Alert.Body)
|
||||
}
|
||||
|
||||
func TestToFirebaseMessage_PollRequest(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ func TestPayments_SubscriptionCreate_NotAStripeCustomer_Success(t *testing.T) {
|
|||
Code: "pro",
|
||||
StripeMonthlyPriceID: "price_123",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
// Create subscription
|
||||
response := request(t, s, "POST", "/v1/account/billing/subscription", `{"tier": "pro", "interval": "month"}`, map[string]string{
|
||||
|
|
@ -184,7 +184,7 @@ func TestPayments_SubscriptionCreate_StripeCustomer_Success(t *testing.T) {
|
|||
Code: "pro",
|
||||
StripeMonthlyPriceID: "price_123",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
|
|
@ -226,7 +226,7 @@ func TestPayments_AccountDelete_Cancels_Subscription(t *testing.T) {
|
|||
Code: "pro",
|
||||
StripeMonthlyPriceID: "price_123",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
|
|
@ -280,7 +280,7 @@ func TestPayments_Checkout_Success_And_Increase_Rate_Limits_Reset_Visitor(t *tes
|
|||
MessageLimit: 220, // 220 * 5% = 11 requests before rate limiting kicks in
|
||||
MessageExpiryDuration: time.Hour,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser)) // No tier
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false)) // No tier
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
|
||||
|
|
@ -461,7 +461,7 @@ func TestPayments_Webhook_Subscription_Updated_Downgrade_From_PastDue_To_Active(
|
|||
AttachmentTotalSizeLimit: 1000000,
|
||||
AttachmentBandwidthLimit: 1000000,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll))
|
||||
require.Nil(t, s.userManager.AddReservation("phil", "ztopic", user.PermissionDenyAll))
|
||||
|
|
@ -570,7 +570,7 @@ func TestPayments_Webhook_Subscription_Deleted(t *testing.T) {
|
|||
StripeMonthlyPriceID: "price_1234",
|
||||
ReservationLimit: 1,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
require.Nil(t, s.userManager.AddReservation("phil", "atopic", user.PermissionDenyAll))
|
||||
|
||||
|
|
@ -658,7 +658,7 @@ func TestPayments_Subscription_Update_Different_Tier(t *testing.T) {
|
|||
StripeMonthlyPriceID: "price_456",
|
||||
StripeYearlyPriceID: "price_457",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{
|
||||
StripeCustomerID: "acct_123",
|
||||
|
|
@ -690,7 +690,7 @@ func TestPayments_Subscription_Delete_At_Period_End(t *testing.T) {
|
|||
Return(&stripe.Subscription{}, nil)
|
||||
|
||||
// Create user
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{
|
||||
StripeCustomerID: "acct_123",
|
||||
StripeSubscriptionID: "sub_123",
|
||||
|
|
@ -724,7 +724,7 @@ func TestPayments_CreatePortalSession(t *testing.T) {
|
|||
}, nil)
|
||||
|
||||
// Create user
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeBilling("phil", &user.Billing{
|
||||
StripeCustomerID: "acct_123",
|
||||
StripeSubscriptionID: "sub_123",
|
||||
|
|
|
|||
|
|
@ -84,6 +84,22 @@ func TestServer_PublishWithFirebase(t *testing.T) {
|
|||
require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"])
|
||||
}
|
||||
|
||||
func TestServer_PublishWithoutFirebase(t *testing.T) {
|
||||
sender := newTestFirebaseSender(10)
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
|
||||
|
||||
response := request(t, s, "PUT", "/mytopic", "my first message", map[string]string{
|
||||
"firebase": "no",
|
||||
})
|
||||
msg1 := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg1.ID)
|
||||
require.Equal(t, "my first message", msg1.Message)
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Firebase publishing happens
|
||||
require.Equal(t, 0, len(sender.Messages()))
|
||||
}
|
||||
|
||||
func TestServer_PublishWithFirebase_WithoutUsers_AndWithoutPanic(t *testing.T) {
|
||||
// This tests issue #641, which used to panic before the fix
|
||||
|
||||
|
|
@ -411,7 +427,7 @@ func TestServer_PublishAt_FromUser(t *testing.T) {
|
|||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfigWithAuthFile(t))
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
response := request(t, s, "PUT", "/mytopic", "a message", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
"In": "1h",
|
||||
|
|
@ -786,7 +802,7 @@ func TestServer_Auth_Success_Admin(t *testing.T) {
|
|||
c := newTestConfigWithAuthFile(t)
|
||||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
|
|
@ -800,7 +816,7 @@ func TestServer_Auth_Success_User(t *testing.T) {
|
|||
c.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
|
||||
|
|
@ -814,7 +830,7 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
|
|||
c.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "mytopic", user.PermissionReadWrite))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "anothertopic", user.PermissionReadWrite))
|
||||
|
||||
|
|
@ -835,7 +851,7 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
|
|||
c.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "INVALID"),
|
||||
|
|
@ -848,7 +864,7 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
|
|||
c.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "sometopic", user.PermissionReadWrite)) // Not mytopic!
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
|
||||
|
|
@ -862,7 +878,7 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
|
|||
c.AuthDefault = user.PermissionReadWrite // Open by default
|
||||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin, false))
|
||||
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "private", user.PermissionDenyAll))
|
||||
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead))
|
||||
|
||||
|
|
@ -911,7 +927,7 @@ func TestServer_Auth_ViaQuery(t *testing.T) {
|
|||
c.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, c)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin, false))
|
||||
|
||||
u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(util.BasicAuth("ben", "some pass"))))
|
||||
response := request(t, s, "GET", u, "", nil)
|
||||
|
|
@ -959,8 +975,8 @@ func TestServer_StatsResetter(t *testing.T) {
|
|||
MessageLimit: 5,
|
||||
MessageExpiryDuration: -5 * time.Second, // Second, what a hack!
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.AddUser("tieruser", "tieruser", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("tieruser", "test"))
|
||||
|
||||
// Send an anonymous message
|
||||
|
|
@ -1104,7 +1120,7 @@ func TestServer_DailyMessageQuotaFromDatabase(t *testing.T) {
|
|||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "test",
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
|
||||
u, err := s.userManager.User("phil")
|
||||
|
|
@ -1684,6 +1700,35 @@ func TestServer_PublishAsJSON_WithActions(t *testing.T) {
|
|||
require.Equal(t, "target_temp_f=65", m.Actions[1].Body)
|
||||
}
|
||||
|
||||
func TestServer_PublishAsJSON_NoCache(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
body := `{"topic":"mytopic","message": "this message is not cached","cache":"no"}`
|
||||
response := request(t, s, "PUT", "/", body, nil)
|
||||
msg := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg.ID)
|
||||
require.Equal(t, "this message is not cached", msg.Message)
|
||||
require.Equal(t, int64(0), msg.Expires)
|
||||
|
||||
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
messages := toMessages(t, response.Body.String())
|
||||
require.Empty(t, messages)
|
||||
}
|
||||
|
||||
func TestServer_PublishAsJSON_WithoutFirebase(t *testing.T) {
|
||||
sender := newTestFirebaseSender(10)
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
s.firebaseClient = newFirebaseClient(sender, &testAuther{Allow: true})
|
||||
|
||||
body := `{"topic":"mytopic","message": "my first message","firebase":"no"}`
|
||||
response := request(t, s, "PUT", "/", body, nil)
|
||||
msg1 := toMessage(t, response.Body.String())
|
||||
require.NotEmpty(t, msg1.ID)
|
||||
require.Equal(t, "my first message", msg1.Message)
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Firebase publishing happens
|
||||
require.Equal(t, 0, len(sender.Messages()))
|
||||
}
|
||||
|
||||
func TestServer_PublishAsJSON_Invalid(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
body := `{"topic":"mytopic",INVALID`
|
||||
|
|
@ -1701,7 +1746,7 @@ func TestServer_PublishWithTierBasedMessageLimitAndExpiry(t *testing.T) {
|
|||
MessageLimit: 5,
|
||||
MessageExpiryDuration: -5 * time.Second, // Second, what a hack!
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
|
||||
// Publish to reach message limit
|
||||
|
|
@ -1937,7 +1982,7 @@ func TestServer_PublishAttachmentWithTierBasedExpiry(t *testing.T) {
|
|||
AttachmentExpiryDuration: sevenDays, // 7 days
|
||||
AttachmentBandwidthLimit: 100000,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
|
||||
// Publish and make sure we can retrieve it
|
||||
|
|
@ -1982,7 +2027,7 @@ func TestServer_PublishAttachmentWithTierBasedBandwidthLimit(t *testing.T) {
|
|||
AttachmentExpiryDuration: time.Hour,
|
||||
AttachmentBandwidthLimit: 14000, // < 3x5000 bytes -> enough for one upload, one download
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
|
||||
// Publish and make sure we can retrieve it
|
||||
|
|
@ -2020,7 +2065,7 @@ func TestServer_PublishAttachmentWithTierBasedLimits(t *testing.T) {
|
|||
AttachmentExpiryDuration: 30 * time.Second,
|
||||
AttachmentBandwidthLimit: 1000000,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
|
||||
// Publish small file as anonymous
|
||||
|
|
@ -2155,7 +2200,7 @@ func TestServer_Visitor_XForwardedFor_None(t *testing.T) {
|
|||
c.BehindProxy = true
|
||||
s := newTestServer(t, c)
|
||||
r, _ := http.NewRequest("GET", "/bla", nil)
|
||||
r.RemoteAddr = "8.9.10.11"
|
||||
r.RemoteAddr = "8.9.10.11:1234"
|
||||
r.Header.Set("X-Forwarded-For", " ") // Spaces, not empty!
|
||||
v, err := s.maybeAuthenticate(r)
|
||||
require.Nil(t, err)
|
||||
|
|
@ -2167,7 +2212,7 @@ func TestServer_Visitor_XForwardedFor_Single(t *testing.T) {
|
|||
c.BehindProxy = true
|
||||
s := newTestServer(t, c)
|
||||
r, _ := http.NewRequest("GET", "/bla", nil)
|
||||
r.RemoteAddr = "8.9.10.11"
|
||||
r.RemoteAddr = "8.9.10.11:1234"
|
||||
r.Header.Set("X-Forwarded-For", "1.1.1.1")
|
||||
v, err := s.maybeAuthenticate(r)
|
||||
require.Nil(t, err)
|
||||
|
|
@ -2179,13 +2224,40 @@ func TestServer_Visitor_XForwardedFor_Multiple(t *testing.T) {
|
|||
c.BehindProxy = true
|
||||
s := newTestServer(t, c)
|
||||
r, _ := http.NewRequest("GET", "/bla", nil)
|
||||
r.RemoteAddr = "8.9.10.11"
|
||||
r.RemoteAddr = "8.9.10.11:1234"
|
||||
r.Header.Set("X-Forwarded-For", "1.2.3.4 , 2.4.4.2,234.5.2.1 ")
|
||||
v, err := s.maybeAuthenticate(r)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "234.5.2.1", v.ip.String())
|
||||
}
|
||||
|
||||
func TestServer_Visitor_Custom_ClientIP_Header(t *testing.T) {
|
||||
c := newTestConfig(t)
|
||||
c.BehindProxy = true
|
||||
c.ProxyForwardedHeader = "X-Client-IP"
|
||||
s := newTestServer(t, c)
|
||||
r, _ := http.NewRequest("GET", "/bla", nil)
|
||||
r.RemoteAddr = "8.9.10.11:1234"
|
||||
r.Header.Set("X-Client-IP", "1.2.3.4")
|
||||
v, err := s.maybeAuthenticate(r)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "1.2.3.4", v.ip.String())
|
||||
}
|
||||
|
||||
func TestServer_Visitor_Custom_Forwarded_Header(t *testing.T) {
|
||||
c := newTestConfig(t)
|
||||
c.BehindProxy = true
|
||||
c.ProxyForwardedHeader = "Forwarded"
|
||||
c.ProxyTrustedAddresses = []string{"1.2.3.4"}
|
||||
s := newTestServer(t, c)
|
||||
r, _ := http.NewRequest("GET", "/bla", nil)
|
||||
r.RemoteAddr = "8.9.10.11:1234"
|
||||
r.Header.Set("Forwarded", " for=5.6.7.8, by=example.com;for=1.2.3.4")
|
||||
v, err := s.maybeAuthenticate(r)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "5.6.7.8", v.ip.String())
|
||||
}
|
||||
|
||||
func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
count := 50000
|
||||
|
|
@ -2242,7 +2314,7 @@ func TestServer_AnonymousUser_And_NonTierUser_Are_Same_Visitor(t *testing.T) {
|
|||
defer s.closeDatabases()
|
||||
|
||||
// Create user without tier
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
|
||||
// Publish a message (anonymous user)
|
||||
rr := request(t, s, "POST", "/mytopic", "hi", nil)
|
||||
|
|
@ -2275,7 +2347,7 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
|
|||
|
||||
// "Register" visitor 1.2.3.4 to topic "upAAAAAAAAAAAA" as a rate limit visitor
|
||||
subscriber1Fn := func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
r.RemoteAddr = "1.2.3.4:1234"
|
||||
}
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriber1Fn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
|
@ -2284,7 +2356,7 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
|
|||
|
||||
// "Register" visitor 8.7.7.1 to topic "up012345678912" as a rate limit visitor (implicitly via topic name)
|
||||
subscriber2Fn := func(r *http.Request) {
|
||||
r.RemoteAddr = "8.7.7.1"
|
||||
r.RemoteAddr = "8.7.7.1:1234"
|
||||
}
|
||||
rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, subscriber2Fn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
|
@ -2327,7 +2399,7 @@ func TestServer_SubscriberRateLimiting_NotWrongTopic(t *testing.T) {
|
|||
s := newTestServer(t, c)
|
||||
|
||||
subscriberFn := func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
r.RemoteAddr = "1.2.3.4:1234"
|
||||
}
|
||||
rr := request(t, s, "GET", "/alerts,upAAAAAAAAAAAA,upBBBBBBBBBBBB/json?poll=1", "", nil, subscriberFn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
|
@ -2347,7 +2419,7 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
|
|||
|
||||
// Registering visitor 1.2.3.4 to topic has no effect
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
r.RemoteAddr = "1.2.3.4:1234"
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "", rr.Body.String())
|
||||
|
|
@ -2355,7 +2427,7 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
|
|||
|
||||
// Registering visitor 8.7.7.1 to topic has no effect
|
||||
rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, func(r *http.Request) {
|
||||
r.RemoteAddr = "8.7.7.1"
|
||||
r.RemoteAddr = "8.7.7.1:1234"
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "", rr.Body.String())
|
||||
|
|
@ -2381,7 +2453,7 @@ func TestServer_SubscriberRateLimiting_UP_Only(t *testing.T) {
|
|||
// "Register" 5 different UnifiedPush visitors
|
||||
for i := 0; i < 5; i++ {
|
||||
subscriberFn := func(r *http.Request) {
|
||||
r.RemoteAddr = fmt.Sprintf("1.2.3.%d", i+1)
|
||||
r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1)
|
||||
}
|
||||
rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, subscriberFn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
|
@ -2405,7 +2477,7 @@ func TestServer_Matrix_SubscriberRateLimiting_UP_Only(t *testing.T) {
|
|||
// "Register" 5 different UnifiedPush visitors
|
||||
for i := 0; i < 5; i++ {
|
||||
rr := request(t, s, "GET", fmt.Sprintf("/up12345678901%d/json?poll=1", i), "", nil, func(r *http.Request) {
|
||||
r.RemoteAddr = fmt.Sprintf("1.2.3.%d", i+1)
|
||||
r.RemoteAddr = fmt.Sprintf("1.2.3.%d:1234", i+1)
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
}
|
||||
|
|
@ -2432,7 +2504,7 @@ func TestServer_SubscriberRateLimiting_VisitorExpiration(t *testing.T) {
|
|||
|
||||
// "Register" rate visitor
|
||||
subscriberFn := func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
r.RemoteAddr = "1.2.3.4:1234"
|
||||
}
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriberFn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
|
|
@ -2471,7 +2543,7 @@ func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *t
|
|||
// - "up123456789012": Allowed, because no ACLs and nobody owns the topic
|
||||
// - "announcements": NOT allowed, because it has read-only permissions for everyone
|
||||
rr := request(t, s, "GET", "/up123456789012,announcements/json?poll=1", "", nil, func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
r.RemoteAddr = "1.2.3.4:1234"
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "1.2.3.4", s.topics["up123456789012"].rateVisitor.ip.String())
|
||||
|
|
@ -2913,7 +2985,7 @@ func request(t *testing.T, s *Server, method, url, body string, headers map[stri
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.RemoteAddr = "9.9.9.9" // Used for tests
|
||||
r.RemoteAddr = "9.9.9.9:1234" // Used for tests
|
||||
for k, v := range headers {
|
||||
r.Header.Set(k, v)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ func TestServer_Twilio_Call_Add_Verify_Call_Delete_Success(t *testing.T) {
|
|||
MessageLimit: 10,
|
||||
CallLimit: 1,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
|
|
@ -140,7 +140,7 @@ func TestServer_Twilio_Call_Success(t *testing.T) {
|
|||
MessageLimit: 10,
|
||||
CallLimit: 1,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
|
|
@ -185,7 +185,7 @@ func TestServer_Twilio_Call_Success_With_Yes(t *testing.T) {
|
|||
MessageLimit: 10,
|
||||
CallLimit: 1,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
u, err := s.userManager.User("phil")
|
||||
require.Nil(t, err)
|
||||
|
|
@ -216,7 +216,7 @@ func TestServer_Twilio_Call_UnverifiedNumber(t *testing.T) {
|
|||
MessageLimit: 10,
|
||||
CallLimit: 1,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "pro"))
|
||||
|
||||
// Do the thing
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) {
|
|||
config.AuthDefault = user.PermissionDenyAll
|
||||
s := newTestServer(t, config)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
|
||||
|
||||
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{
|
||||
|
|
@ -126,7 +126,7 @@ func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) {
|
|||
config := configureAuth(t, newTestConfigWithWebPush(t))
|
||||
s := newTestServer(t, config)
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
|
||||
|
||||
response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{
|
||||
|
|
@ -212,7 +212,7 @@ func TestServer_WebPush_Expiry(t *testing.T) {
|
|||
addSubscription(t, s, pushService.URL+"/push-receive", "test-topic")
|
||||
requireSubscriptionCount(t, s, "test-topic", 1)
|
||||
|
||||
_, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-7*24*time.Hour).Unix())
|
||||
_, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-55*24*time.Hour).Unix())
|
||||
require.Nil(t, err)
|
||||
|
||||
s.pruneAndNotifyWebPushSubscriptions()
|
||||
|
|
@ -222,7 +222,7 @@ func TestServer_WebPush_Expiry(t *testing.T) {
|
|||
return received.Load()
|
||||
})
|
||||
|
||||
_, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-9*24*time.Hour).Unix())
|
||||
_, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-60*24*time.Hour).Unix())
|
||||
require.Nil(t, err)
|
||||
|
||||
s.pruneAndNotifyWebPushSubscriptions()
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ type publishMessage struct {
|
|||
Filename string `json:"filename"`
|
||||
Email string `json:"email"`
|
||||
Call string `json:"call"`
|
||||
Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead)
|
||||
Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead)
|
||||
Delay string `json:"delay"`
|
||||
}
|
||||
|
||||
|
|
@ -253,9 +255,10 @@ type apiStatsResponse struct {
|
|||
MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
|
||||
}
|
||||
|
||||
type apiUserAddRequest struct {
|
||||
type apiUserAddOrUpdateRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Hash string `json:"hash"`
|
||||
Tier string `json:"tier"`
|
||||
// Do not add 'role' here. We don't want to add admins via the API.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,18 @@ import (
|
|||
"net/http"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
mimeDecoder mime.WordDecoder
|
||||
mimeDecoder mime.WordDecoder
|
||||
|
||||
// priorityHeaderIgnoreRegex matches specific patterns of the "Priority" header (RFC 9218), so that it can be ignored
|
||||
priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`)
|
||||
|
||||
// forwardedHeaderRegex parses IPv4 addresses from the "Forwarded" header (RFC 7239)
|
||||
forwardedHeaderRegex = regexp.MustCompile(`(?i)\bfor="?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"?`)
|
||||
)
|
||||
|
||||
func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
|
||||
|
|
@ -34,15 +40,11 @@ func toBool(value string) bool {
|
|||
return value == "1" || value == "yes" || value == "true"
|
||||
}
|
||||
|
||||
func readCommaSeparatedParam(r *http.Request, names ...string) (params []string) {
|
||||
paramStr := readParam(r, names...)
|
||||
if paramStr != "" {
|
||||
params = make([]string, 0)
|
||||
for _, s := range util.SplitNoEmpty(paramStr, ",") {
|
||||
params = append(params, strings.TrimSpace(s))
|
||||
}
|
||||
func readCommaSeparatedParam(r *http.Request, names ...string) []string {
|
||||
if paramStr := readParam(r, names...); paramStr != "" {
|
||||
return util.Map(util.SplitNoEmpty(paramStr, ","), strings.TrimSpace)
|
||||
}
|
||||
return params
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func readParam(r *http.Request, names ...string) string {
|
||||
|
|
@ -73,34 +75,58 @@ func readQueryParam(r *http.Request, names ...string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr {
|
||||
remoteAddr := r.RemoteAddr
|
||||
addrPort, err := netip.ParseAddrPort(remoteAddr)
|
||||
ip := addrPort.Addr()
|
||||
// extractIPAddress extracts the IP address of the visitor from the request,
|
||||
// either from the TCP socket or from a proxy header.
|
||||
func extractIPAddress(r *http.Request, behindProxy bool, proxyForwardedHeader string, proxyTrustedAddresses []string) netip.Addr {
|
||||
if behindProxy && proxyForwardedHeader != "" {
|
||||
if addr, err := extractIPAddressFromHeader(r, proxyForwardedHeader, proxyTrustedAddresses); err == nil {
|
||||
return addr
|
||||
}
|
||||
// Fall back to the remote address if the header is not found or invalid
|
||||
}
|
||||
addrPort, err := netip.ParseAddrPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// This should not happen in real life; only in tests. So, using falling back to 0.0.0.0 if address unspecified
|
||||
ip, err = netip.ParseAddr(remoteAddr)
|
||||
if err != nil {
|
||||
ip = netip.IPv4Unspecified()
|
||||
if remoteAddr != "@" && !behindProxy { // RemoteAddr is @ when unix socket is used
|
||||
logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", remoteAddr)
|
||||
logr(r).Err(err).Warn("unable to parse IP (%s), new visitor with unspecified IP (0.0.0.0) created", r.RemoteAddr)
|
||||
return netip.IPv4Unspecified()
|
||||
}
|
||||
return addrPort.Addr()
|
||||
}
|
||||
|
||||
// extractIPAddressFromHeader extracts the last IP address from the specified header.
|
||||
//
|
||||
// It supports multiple formats:
|
||||
// - single IP address
|
||||
// - comma-separated list
|
||||
// - RFC 7239-style list (Forwarded header)
|
||||
//
|
||||
// If there are multiple addresses, we first remove the trusted IP addresses from the list, and
|
||||
// then take the right-most address in the list (as this is the one added by our proxy server).
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details.
|
||||
func extractIPAddressFromHeader(r *http.Request, forwardedHeader string, trustedAddresses []string) (netip.Addr, error) {
|
||||
value := strings.TrimSpace(strings.ToLower(r.Header.Get(forwardedHeader)))
|
||||
if value == "" {
|
||||
return netip.IPv4Unspecified(), fmt.Errorf("no %s header found", forwardedHeader)
|
||||
}
|
||||
// Extract valid addresses
|
||||
addrsStrs := util.Map(util.SplitNoEmpty(value, ","), strings.TrimSpace)
|
||||
var validAddrs []netip.Addr
|
||||
for _, addrStr := range addrsStrs {
|
||||
if addr, err := netip.ParseAddr(addrStr); err == nil {
|
||||
validAddrs = append(validAddrs, addr)
|
||||
} else if m := forwardedHeaderRegex.FindStringSubmatch(addrStr); len(m) == 2 {
|
||||
if addr, err := netip.ParseAddr(m[1]); err == nil {
|
||||
validAddrs = append(validAddrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
if behindProxy && strings.TrimSpace(r.Header.Get("X-Forwarded-For")) != "" {
|
||||
// X-Forwarded-For can contain multiple addresses (see #328). If we are behind a proxy,
|
||||
// only the right-most address can be trusted (as this is the one added by our proxy server).
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For for details.
|
||||
ips := util.SplitNoEmpty(r.Header.Get("X-Forwarded-For"), ",")
|
||||
realIP, err := netip.ParseAddr(strings.TrimSpace(util.LastString(ips, remoteAddr)))
|
||||
if err != nil {
|
||||
logr(r).Err(err).Error("invalid IP address %s received in X-Forwarded-For header", ip)
|
||||
// Fall back to regular remote address if X-Forwarded-For is damaged
|
||||
} else {
|
||||
ip = realIP
|
||||
}
|
||||
// Filter out proxy addresses
|
||||
clientAddrs := util.Filter(validAddrs, func(addr netip.Addr) bool {
|
||||
return !slices.Contains(trustedAddresses, addr.String())
|
||||
})
|
||||
if len(clientAddrs) == 0 {
|
||||
return netip.IPv4Unspecified(), fmt.Errorf("no client IP address found in %s header: %s", forwardedHeader, value)
|
||||
}
|
||||
return ip
|
||||
return clientAddrs[len(clientAddrs)-1], nil
|
||||
}
|
||||
|
||||
func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) {
|
||||
|
|
@ -133,7 +159,7 @@ func fromContext[T any](r *http.Request, key contextKey) (T, error) {
|
|||
|
||||
// maybeDecodeHeader decodes the given header value if it is MIME encoded, e.g. "=?utf-8?q?Hello_World?=",
|
||||
// or returns the original header value if it is not MIME encoded. It also calls maybeIgnoreSpecialHeader
|
||||
// to ignore new HTTP "Priority" header.
|
||||
// to ignore the new HTTP "Priority" header.
|
||||
func maybeDecodeHeader(name, value string) string {
|
||||
decoded, err := mimeDecoder.DecodeHeader(value)
|
||||
if err != nil {
|
||||
|
|
@ -142,7 +168,7 @@ func maybeDecodeHeader(name, value string) string {
|
|||
return maybeIgnoreSpecialHeader(name, decoded)
|
||||
}
|
||||
|
||||
// maybeIgnoreSpecialHeader ignores new HTTP "Priority" header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority)
|
||||
// maybeIgnoreSpecialHeader ignores the new HTTP "Priority" header (RFC 9218, see https://datatracker.ietf.org/doc/html/rfc9218)
|
||||
//
|
||||
// Cloudflare (and potentially other providers) add this to requests when forwarding to the backend (ntfy),
|
||||
// so we just ignore it. If the "Priority" header is set to "u=*, i" or "u=*" (by Cloudflare), the header will be ignored.
|
||||
|
|
|
|||
|
|
@ -88,3 +88,33 @@ func TestMaybeDecodeHeaders(t *testing.T) {
|
|||
r.Header.Set("X-Priority", "5") // ntfy priority header
|
||||
require.Equal(t, "5", readHeaderParam(r, "x-priority", "priority", "p"))
|
||||
}
|
||||
|
||||
func TestExtractIPAddress(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil)
|
||||
r.RemoteAddr = "10.0.0.1:1234"
|
||||
r.Header.Set("X-Forwarded-For", " 1.2.3.4 , 5.6.7.8")
|
||||
r.Header.Set("X-Client-IP", "9.10.11.12")
|
||||
r.Header.Set("X-Real-IP", "13.14.15.16, 1.1.1.1")
|
||||
r.Header.Set("Forwarded", "for=17.18.19.20;by=proxy.example.com, by=2.2.2.2;for=1.1.1.1")
|
||||
|
||||
trustedProxies := []string{"1.1.1.1"}
|
||||
|
||||
require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String())
|
||||
require.Equal(t, "9.10.11.12", extractIPAddress(r, true, "X-Client-IP", trustedProxies).String())
|
||||
require.Equal(t, "13.14.15.16", extractIPAddress(r, true, "X-Real-IP", trustedProxies).String())
|
||||
require.Equal(t, "17.18.19.20", extractIPAddress(r, true, "Forwarded", trustedProxies).String())
|
||||
require.Equal(t, "10.0.0.1", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String())
|
||||
}
|
||||
|
||||
func TestExtractIPAddress_UnixSocket(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil)
|
||||
r.RemoteAddr = "@"
|
||||
r.Header.Set("X-Forwarded-For", "1.2.3.4, 5.6.7.8, 1.1.1.1")
|
||||
r.Header.Set("Forwarded", "by=bla.example.com;for=17.18.19.20")
|
||||
|
||||
trustedProxies := []string{"1.1.1.1"}
|
||||
|
||||
require.Equal(t, "5.6.7.8", extractIPAddress(r, true, "X-Forwarded-For", trustedProxies).String())
|
||||
require.Equal(t, "17.18.19.20", extractIPAddress(r, true, "Forwarded", trustedProxies).String())
|
||||
require.Equal(t, "0.0.0.0", extractIPAddress(r, false, "X-Forwarded-For", trustedProxies).String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ const (
|
|||
deleteWebPushSubscriptionByUserIDQuery = `DELETE FROM subscription WHERE user_id = ?`
|
||||
deleteWebPushSubscriptionByAgeQuery = `DELETE FROM subscription WHERE updated_at <= ?` // Full table scan!
|
||||
|
||||
insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)`
|
||||
deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?`
|
||||
insertWebPushSubscriptionTopicQuery = `INSERT INTO subscription_topic (subscription_id, topic) VALUES (?, ?)`
|
||||
deleteWebPushSubscriptionTopicAllQuery = `DELETE FROM subscription_topic WHERE subscription_id = ?`
|
||||
deleteWebPushSubscriptionTopicWithoutSubscription = `DELETE FROM subscription_topic WHERE subscription_id NOT IN (SELECT id FROM subscription)`
|
||||
)
|
||||
|
||||
// Schema management queries
|
||||
|
|
@ -271,6 +272,10 @@ func (c *webPushStore) RemoveSubscriptionsByUserID(userID string) error {
|
|||
// RemoveExpiredSubscriptions removes all subscriptions that have not been updated for a given time period
|
||||
func (c *webPushStore) RemoveExpiredSubscriptions(expireAfter time.Duration) error {
|
||||
_, err := c.db.Exec(deleteWebPushSubscriptionByAgeQuery, time.Now().Add(-expireAfter).Unix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.db.Exec(deleteWebPushSubscriptionTopicWithoutSubscription)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const (
|
|||
userHardDeleteAfterDuration = 7 * 24 * time.Hour
|
||||
tokenPrefix = "tk_"
|
||||
tokenLength = 32
|
||||
tokenMaxCount = 20 // Only keep this many tokens in the table per user
|
||||
tokenMaxCount = 60 // Only keep this many tokens in the table per user
|
||||
tag = "user_manager"
|
||||
)
|
||||
|
||||
|
|
@ -864,13 +864,19 @@ func (a *Manager) resolvePerms(base, perm Permission) error {
|
|||
}
|
||||
|
||||
// AddUser adds a user with the given username, password and role
|
||||
func (a *Manager) AddUser(username, password string, role Role) error {
|
||||
func (a *Manager) AddUser(username, password string, role Role, hashed bool) error {
|
||||
if !AllowedUsername(username) || !AllowedRole(role) {
|
||||
return ErrInvalidArgument
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
var hash []byte
|
||||
var err error = nil
|
||||
if hashed {
|
||||
hash = []byte(password)
|
||||
} else {
|
||||
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
userID := util.RandomStringPrefix(userIDPrefix, userIDLength)
|
||||
syncTopic, now := util.RandomStringPrefix(syncTopicPrefix, syncTopicLength), time.Now().Unix()
|
||||
|
|
@ -1192,10 +1198,17 @@ func (a *Manager) ReservationOwner(topic string) (string, error) {
|
|||
}
|
||||
|
||||
// ChangePassword changes a user's password
|
||||
func (a *Manager) ChangePassword(username, password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
func (a *Manager) ChangePassword(username, password string, hashed bool) error {
|
||||
var hash []byte
|
||||
var err error
|
||||
|
||||
if hashed {
|
||||
hash = []byte(password)
|
||||
} else {
|
||||
hash, err = bcrypt.GenerateFromPassword([]byte(password), a.bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := a.db.Exec(updateUserPassQuery, hash, username); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources
|
||||
const minBcryptTimingMillis = int64(40) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources
|
||||
|
||||
func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
|
||||
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("john", "john", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
require.Nil(t, a.AddUser("john", "john", RoleUser, false))
|
||||
require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite))
|
||||
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
|
||||
require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite))
|
||||
|
|
@ -134,7 +134,7 @@ func TestManager_Access_Order_LengthWriteRead(t *testing.T) {
|
|||
// and longer ACL rules are prioritized as well.
|
||||
|
||||
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
require.Nil(t, a.AllowAccess("ben", "test*", PermissionReadWrite))
|
||||
require.Nil(t, a.AllowAccess("ben", "*", PermissionRead))
|
||||
|
||||
|
|
@ -147,20 +147,20 @@ func TestManager_Access_Order_LengthWriteRead(t *testing.T) {
|
|||
|
||||
func TestManager_AddUser_Invalid(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin))
|
||||
require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role"))
|
||||
require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin, false))
|
||||
require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role", false))
|
||||
}
|
||||
|
||||
func TestManager_AddUser_Timing(t *testing.T) {
|
||||
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
|
||||
start := time.Now().UnixMilli()
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false))
|
||||
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
|
||||
}
|
||||
|
||||
func TestManager_AddUser_And_Query(t *testing.T) {
|
||||
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false))
|
||||
require.Nil(t, a.ChangeBilling("user", &Billing{
|
||||
StripeCustomerID: "acct_123",
|
||||
StripeSubscriptionID: "sub_123",
|
||||
|
|
@ -187,7 +187,7 @@ func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) {
|
|||
a := newTestManager(t, PermissionDenyAll)
|
||||
|
||||
// Create user, add reservations and token
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false))
|
||||
require.Nil(t, a.AddReservation("user", "mytopic", PermissionRead))
|
||||
|
||||
u, err := a.User("user")
|
||||
|
|
@ -237,7 +237,7 @@ func TestManager_CreateToken_Only_Lower(t *testing.T) {
|
|||
a := newTestManager(t, PermissionDenyAll)
|
||||
|
||||
// Create user, add reservations and token
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin, false))
|
||||
u, err := a.User("user")
|
||||
require.Nil(t, err)
|
||||
|
||||
|
|
@ -248,8 +248,8 @@ func TestManager_CreateToken_Only_Lower(t *testing.T) {
|
|||
|
||||
func TestManager_UserManagement(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite))
|
||||
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
|
||||
require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite))
|
||||
|
|
@ -339,21 +339,31 @@ func TestManager_UserManagement(t *testing.T) {
|
|||
|
||||
func TestManager_ChangePassword(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, false))
|
||||
require.Nil(t, a.AddUser("jane", "$2b$10$OyqU72muEy7VMd1SAU2Iru5IbeSMgrtCGHu/fWLmxL1MwlijQXWbG", RoleUser, true))
|
||||
|
||||
_, err := a.Authenticate("phil", "phil")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Nil(t, a.ChangePassword("phil", "newpass"))
|
||||
_, err = a.Authenticate("jane", "jane")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Nil(t, a.ChangePassword("phil", "newpass", false))
|
||||
_, err = a.Authenticate("phil", "phil")
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
_, err = a.Authenticate("phil", "newpass")
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Nil(t, a.ChangePassword("jane", "$2b$10$CNaCW.q1R431urlbQ5Drh.zl48TiiOeJSmZgfcswkZiPbJGQ1ApSS", true))
|
||||
_, err = a.Authenticate("jane", "jane")
|
||||
require.Equal(t, ErrUnauthenticated, err)
|
||||
_, err = a.Authenticate("jane", "newpass")
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestManager_ChangeRole(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
require.Nil(t, a.AllowAccess("ben", "mytopic", PermissionReadWrite))
|
||||
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
|
||||
|
||||
|
|
@ -378,8 +388,8 @@ func TestManager_ChangeRole(t *testing.T) {
|
|||
|
||||
func TestManager_Reservations(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
require.Nil(t, a.AddReservation("ben", "ztopic_", PermissionDenyAll))
|
||||
require.Nil(t, a.AddReservation("ben", "readme", PermissionRead))
|
||||
require.Nil(t, a.AllowAccess("ben", "something-else", PermissionRead))
|
||||
|
|
@ -460,7 +470,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
|
|||
AttachmentTotalSizeLimit: 524288000,
|
||||
AttachmentExpiryDuration: 24 * time.Hour,
|
||||
}))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
require.Nil(t, a.ChangeTier("ben", "pro"))
|
||||
require.Nil(t, a.AddReservation("ben", "mytopic", PermissionDenyAll))
|
||||
|
||||
|
|
@ -507,7 +517,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
|
|||
|
||||
func TestManager_Token_Valid(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
|
||||
u, err := a.User("ben")
|
||||
require.Nil(t, err)
|
||||
|
|
@ -551,7 +561,7 @@ func TestManager_Token_Valid(t *testing.T) {
|
|||
|
||||
func TestManager_Token_Invalid(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
|
||||
u, err := a.AuthenticateToken(strings.Repeat("x", 32)) // 32 == token length
|
||||
require.Nil(t, u)
|
||||
|
|
@ -570,7 +580,7 @@ func TestManager_Token_NotFound(t *testing.T) {
|
|||
|
||||
func TestManager_Token_Expire(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
|
||||
u, err := a.User("ben")
|
||||
require.Nil(t, err)
|
||||
|
|
@ -618,7 +628,7 @@ func TestManager_Token_Expire(t *testing.T) {
|
|||
|
||||
func TestManager_Token_Extend(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
|
||||
// Try to extend token for user without token
|
||||
u, err := a.User("ben")
|
||||
|
|
@ -647,8 +657,8 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
|
|||
// Tests that tokens are automatically deleted when the maximum number of tokens is reached
|
||||
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
|
||||
|
||||
ben, err := a.User("ben")
|
||||
require.Nil(t, err)
|
||||
|
|
@ -668,10 +678,10 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
|
|||
require.NotEmpty(t, token.Value)
|
||||
philTokens = append(philTokens, token.Value)
|
||||
|
||||
// Create 22 tokens for ben (only 20 allowed!)
|
||||
// Create 62 tokens for ben (only 60 allowed!)
|
||||
baseTime := time.Now().Add(24 * time.Hour)
|
||||
benTokens := make([]string, 0)
|
||||
for i := 0; i < 22; i++ { //
|
||||
for i := 0; i < 62; i++ { //
|
||||
token, err := a.CreateToken(ben.ID, "", time.Now().Add(72*time.Hour), netip.IPv4Unspecified())
|
||||
require.Nil(t, err)
|
||||
require.NotEmpty(t, token.Value)
|
||||
|
|
@ -690,7 +700,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
|
|||
require.Equal(t, ErrUnauthenticated, err)
|
||||
|
||||
// Ben: The other tokens should still work
|
||||
for i := 2; i < 22; i++ {
|
||||
for i := 2; i < 62; i++ {
|
||||
userWithToken, err := a.AuthenticateToken(benTokens[i])
|
||||
require.Nil(t, err, "token[%d]=%s failed", i, benTokens[i])
|
||||
require.Equal(t, "ben", userWithToken.Name)
|
||||
|
|
@ -710,7 +720,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
|
|||
require.Nil(t, err)
|
||||
require.True(t, rows.Next())
|
||||
require.Nil(t, rows.Scan(&benCount))
|
||||
require.Equal(t, 20, benCount)
|
||||
require.Equal(t, 60, benCount)
|
||||
|
||||
var philCount int
|
||||
rows, err = a.db.Query(`SELECT COUNT(*) FROM user_token WHERE user_id=?`, phil.ID)
|
||||
|
|
@ -723,7 +733,7 @@ func TestManager_Token_MaxCount_AutoDelete(t *testing.T) {
|
|||
func TestManager_EnqueueStats_ResetStats(t *testing.T) {
|
||||
a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
|
||||
// Baseline: No messages or emails
|
||||
u, err := a.User("ben")
|
||||
|
|
@ -765,7 +775,7 @@ func TestManager_EnqueueStats_ResetStats(t *testing.T) {
|
|||
func TestManager_EnqueueTokenUpdate(t *testing.T) {
|
||||
a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 500*time.Millisecond)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
|
||||
// Create user and token
|
||||
u, err := a.User("ben")
|
||||
|
|
@ -798,7 +808,7 @@ func TestManager_EnqueueTokenUpdate(t *testing.T) {
|
|||
func TestManager_ChangeSettings(t *testing.T) {
|
||||
a, err := NewManager(filepath.Join(t.TempDir(), "db"), "", PermissionReadWrite, bcrypt.MinCost, 1500*time.Millisecond)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
|
||||
// No settings
|
||||
u, err := a.User("ben")
|
||||
|
|
@ -866,7 +876,7 @@ func TestManager_Tier_Create_Update_List_Delete(t *testing.T) {
|
|||
AttachmentBandwidthLimit: 21474836480,
|
||||
StripeMonthlyPriceID: "price_2",
|
||||
}))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
|
||||
require.Nil(t, a.ChangeTier("phil", "pro"))
|
||||
|
||||
ti, err := a.Tier("pro")
|
||||
|
|
@ -981,7 +991,7 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) {
|
|||
Name: "Pro",
|
||||
ReservationLimit: 4,
|
||||
}))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
|
||||
require.Nil(t, a.ChangeTier("phil", "pro"))
|
||||
|
||||
// Add 10 reservations (pro tier allows that)
|
||||
|
|
@ -1007,7 +1017,7 @@ func TestManager_Tier_Change_And_Reset(t *testing.T) {
|
|||
func TestUser_PhoneNumberAddListRemove(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
|
||||
phil, err := a.User("phil")
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, a.AddPhoneNumber(phil.ID, "+1234567890"))
|
||||
|
|
@ -1032,8 +1042,8 @@ func TestUser_PhoneNumberAddListRemove(t *testing.T) {
|
|||
func TestUser_PhoneNumberAdd_Multiple_Users_Same_Number(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser, false))
|
||||
phil, err := a.User("phil")
|
||||
require.Nil(t, err)
|
||||
ben, err := a.User("ben")
|
||||
|
|
|
|||
27
util/util.go
27
util/util.go
|
|
@ -17,10 +17,9 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"golang.org/x/term"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -99,12 +98,26 @@ func SplitKV(s string, sep string) (key string, value string) {
|
|||
return "", strings.TrimSpace(kv[0])
|
||||
}
|
||||
|
||||
// LastString returns the last string in a slice, or def if s is empty
|
||||
func LastString(s []string, def string) string {
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
// Map applies a function to each element of a slice and returns a new slice with the results
|
||||
// Example: Map([]int{1, 2, 3}, func(i int) int { return i * 2 }) -> []int{2, 4, 6}
|
||||
func Map[T any, U any](slice []T, f func(T) U) []U {
|
||||
result := make([]U, len(slice))
|
||||
for i, v := range slice {
|
||||
result[i] = f(v)
|
||||
}
|
||||
return s[len(s)-1]
|
||||
return result
|
||||
}
|
||||
|
||||
// Filter returns a new slice containing only the elements of the original slice for which the
|
||||
// given function returns true.
|
||||
func Filter[T any](slice []T, f func(T) bool) []T {
|
||||
result := make([]T, 0)
|
||||
for _, v := range slice {
|
||||
if f(v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RandomString returns a random string with a given length
|
||||
|
|
|
|||
|
|
@ -167,11 +167,6 @@ func TestSplitKV(t *testing.T) {
|
|||
require.Equal(t, "value=with=separator", value)
|
||||
}
|
||||
|
||||
func TestLastString(t *testing.T) {
|
||||
require.Equal(t, "last", LastString([]string{"first", "second", "last"}, "default"))
|
||||
require.Equal(t, "default", LastString([]string{}, "default"))
|
||||
}
|
||||
|
||||
func TestQuoteCommand(t *testing.T) {
|
||||
require.Equal(t, `ls -al "Document Folder"`, QuoteCommand([]string{"ls", "-al", "Document Folder"}))
|
||||
require.Equal(t, `rsync -av /home/phil/ root@example.com:/home/phil/`, QuoteCommand([]string{"rsync", "-av", "/home/phil/", "root@example.com:/home/phil/"}))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -44,8 +44,8 @@
|
|||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"prettier": "^2.8.8",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-pwa": "^0.15.0"
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-pwa": "^1.0.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
|
|||
Loading…
Reference in New Issue