diff --git a/cmd/publish.go b/cmd/publish.go index c2860b4..89e2ca9 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -25,7 +25,7 @@ var cmdPublish = &cli.Command{ &cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"}, &cli.StringFlag{Name: "click", Aliases: []string{"U"}, Usage: "URL to open when notification is clicked"}, &cli.StringFlag{Name: "attach", Aliases: []string{"a"}, Usage: "URL to send as an external attachment"}, - &cli.StringFlag{Name: "filename", Aliases: []string{"n"}, Usage: "Filename for the attachment"}, + &cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, Usage: "Filename for the attachment"}, &cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "File to upload as an attachment"}, &cli.StringFlag{Name: "email", Aliases: []string{"e-mail", "mail", "e"}, Usage: "also send to e-mail address"}, &cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"}, diff --git a/docs/config.md b/docs/config.md index c608fd3..7609b5b 100644 --- a/docs/config.md +++ b/docs/config.md @@ -36,13 +36,13 @@ Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscri [`since=` parameter](subscribe/api.md#fetch-cached-messages). ## Attachments -If desired, you may allow users to upload and [attach files to notifications](publish.md#attachments-send-files). To enable +If desired, you may allow users to upload and [attach files to notifications](publish.md#attachments). To enable this feature, you have to simply configure an attachment cache directory and a base URL (`attachment-cache-dir`, `base-url`). Once these options are set and the directory is writable by the server user, you can upload attachments via PUT. -By default, attachments are stored in the disk-case **for only 3 hours**. The main reason for this is to avoid legal issues -and such when hosting user controlled content. Typically, this is more than enough time for the user (or the phone) to download -the file. The following config options are relevant to attachments: +By default, attachments are stored in the disk-cache **for only 3 hours**. The main reason for this is to avoid legal issues +and such when hosting user controlled content. Typically, this is more than enough time for the user (or the auto download +feature) to download the file. The following config options are relevant to attachments: * `base-url` is the root URL for the ntfy server; this is needed for the generated attachment URLs * `attachment-cache-dir` is the cache directory for attached files @@ -356,8 +356,15 @@ request every 10s (defined by `visitor-request-limit-replenish`) * `visitor-request-limit-replenish` is the rate at which the bucket is refilled (one request per x). Defaults to 10s. ### Attachment limits +Aside from the global file size and total attachment cache limits (see [above](#attachments)), there are two relevant +per-visitor limits: -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx +* `visitor-attachment-total-size-limit` is the total storage limit used for attachments per visitor. It defaults to 100M. + The per-visitor storage is automatically decreased as attachments expire. External attachments (attached via `X-Attach`, + see [publishing docs](publish.md#attachments)) do not count here. +* `visitor-attachment-daily-bandwidth-limit` is the total daily attachment download/upload bandwidth limit per visitor, + including PUT and GET requests. This is to protect your precious bandwidth from abuse, since egress costs money in + most cloud providers. This defaults to 500M. ### E-mail limits Similarly to the request limit, there is also an e-mail limit (only relevant if [e-mail notifications](#e-mail-notifications) @@ -470,38 +477,38 @@ Each config option can be set in the config file `/etc/ntfy/server.yml` (e.g. `l CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`). -| Config option | Env variable | Format | Default | Description | -|---|---|---|---|---| -| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) | -| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server | -| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. | -| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | -| `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). | -| `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. | -| `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. | -| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. | -| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 55s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. | -| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. | -| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending | -| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled | -| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled | -| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled | -| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` | -| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` | -| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` | -| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. | -| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) | -| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. | -| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. | -| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has | -| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 10s | Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled | -| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Initial limit of e-mails per visitor | -| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled | -| `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. | +| Config option | Env variable | Format | Default | Description | +|--------------------------------------------|-------------------------------------------------|------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `base-url` | `NTFY_BASE_URL` | *URL* | - | Public facing base URL of the service (e.g. `https://ntfy.sh`) | +| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server | +| `listen-https` | `NTFY_LISTEN_HTTPS` | `[host]:port` | - | Listen address for the HTTPS web server. If set, you also need to set `key-file` and `cert-file`. | +| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. | +| `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). | +| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). | +| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. | +| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. | +| `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. | +| `attachment-expiry-duration` | `NTFY_ATTACHMENT_EXPIRY_DURATION` | *duration* | 3h | Duration after which uploaded attachments will be deleted (e.g. 3h, 20h). Strongly affects `visitor-attachment-total-size-limit`. | +| `smtp-sender-addr` | `NTFY_SMTP_SENDER_ADDR` | `host:port` | - | SMTP server address to allow email sending | +| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled | +| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled | +| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled | +| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` | +| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` | +| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` | +| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 55s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. | +| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. | +| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. | +| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) | +| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. | +| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. | +| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has | +| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 10s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled | +| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor | +| `visitor-email-limit-replenish` | `NTFY_VISITOR_EMAIL_LIMIT_REPLENISH` | *duration* | 1h | Rate limiting: Strongly related to `visitor-email-limit-burst`: The rate at which the bucket is refilled | The format for a *duration* is: `(smh)`, e.g. 30s, 20m or 1h. The format for a *size* is: `(GMK)`, e.g. 1G, 200M or 4000k. diff --git a/docs/publish.md b/docs/publish.md index ace4c59..2d5369a 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -659,26 +659,33 @@ Here's an example that will open Reddit when the notification is clicked: ])); ``` -## Attachments (send files) +## Attachments You can send images and other files to your phone as attachments to a notification. The attachments are then downloaded onto your phone (depending on size and setting automatically), and can be used from the Downloads folder. -There are two different ways to send attachments, either via PUT or by passing an external URL. +There are two different ways to send attachments: -**Upload attachments from your computer**: To send an attachment from your computer as a file, you can send it as the -PUT request body. If a message is greater than the maximum message size or consists of non-UTF-8 characters, the ntfy -server will automatically detect the mime type and size, and send the message as an attachment file. +* sending [a local file](#attach-local-file) via PUT, e.g. from `~/Flowers/flower.jpg` or `ringtone.mp3` +* or by [passing an external URL](#attach-file-from-a-url) as an attachment, e.g. `https://f-droid.org/F-Droid.apk` -You can optionally pass a filename (or force attachment mode for small text-messages) by passing the `X-Filename` header -or query parameter (or any of its aliases `Filename`, `File` or `f`). +### Attach local file +To send an attachment from your computer as a file, you can send it as the PUT request body. If a message is greater +than the maximum message size (4,096 bytes) or consists of non UTF-8 characters, the ntfy server will automatically +detect the mime type and size, and send the message as an attachment file. To send smaller text-only messages or files +as attachments, you must pass a filename by passing the `X-Filename` header or query parameter (or any of its aliases +`Filename`, `File` or `f`). + +By default, and how ntfy.sh is configured, the **max attachment size is 15 MB** (with 100 MB total per visitor). +Attachments **expire after 3 hours**, which typically is plenty of time for the user to download it, or for the Android app +to auto-download it. Please also check out the [other limits below](#limitations). Here's an example showing how to upload an image: - === "Command line (curl)" ``` curl \ -T flower.jpg \ + -H "Filename: flower.jpg" \ ntfy.sh/flowers ``` @@ -693,6 +700,7 @@ Here's an example showing how to upload an image: ``` http PUT /flowers HTTP/1.1 Host: ntfy.sh + Filename: flower.jpg ``` @@ -701,7 +709,8 @@ Here's an example showing how to upload an image: ``` javascript fetch('https://ntfy.sh/flowers', { method: 'PUT', - body: document.getElementById("file").files[0] + body: document.getElementById("file").files[0], + headers: { 'Filename': 'flower.jpg' } }) ``` @@ -709,44 +718,108 @@ Here's an example showing how to upload an image: ``` go file, _ := os.Open("flower.jpg") req, _ := http.NewRequest("PUT", "https://ntfy.sh/flowers", file) + req.Header.Set("Filename", "flower.jpg") http.DefaultClient.Do(req) ``` === "Python" ``` python requests.put("https://ntfy.sh/flowers", - data=open("flower.jpg", 'rb')) + data=open("flower.jpg", 'rb'), + headers={ "Filename": "flower.jpg" }) ``` === "PHP" ``` php-inline - file_get_contents('https://ntfy.sh/reddit_alerts', false, stream_context_create([ + file_get_contents('https://ntfy.sh/flowers', false, stream_context_create([ 'http' => [ 'method' => 'PUT', - 'content' => XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx + 'header' => + "Content-Type: application/octet-stream\r\n" . // Does not matter + "Filename: flower.jpg", + 'content' => file_get_contents('flower.jpg') // Dangerous for large files ] ])); ``` -``` -- Uploaded attachment -- External attachment -- Preview without attachment +Here's what that looks like on Android: +
+ ![image attachment](static/img/android-screenshot-attachment-image.png){ width=500 } +
Image attachment sent from a local file
+
-# Upload and send attachment with custom message and filename -curl \ - -T flower.jpg \ - -H "Message: Here's a flower for you" \ - -H "Filename: flower.jpg" \ - ntfy.sh/howdy +### Attach file from a URL +Instead of sending a local file to your phone, you can use an external URL to specify where the attachment is hosted. +This could be a Google Drive or Dropbox link, or any other publicly available URL. The ntfy server will briefly probe +the URL to retrieve type and size for you. Since the files are externally hosted, the expiration or size limits from +above do not apply here. -# Send external attachment from other URL, with custom message -curl \ - -H "Attachment: https://example.com/files.zip" \ - "ntfy.sh/howdy?m=Important+documents+attached" +To attach an external file, simple pass the `X-Attach` header or query parameter (or any of its aliases `Attach` or `a`) +to specify the attachment URL. It can be any type of file. + +Here's an example showing how to upload an image: + +=== "Command line (curl)" + ``` + curl \ + -X POST \ + -H "Attach: https://f-droid.org/F-Droid.apk" \ + ntfy.sh/mydownloads + ``` + +=== "ntfy CLI" + ``` + ntfy publish \ + --attach="https://f-droid.org/F-Droid.apk" \ + mydownloads + ``` + +=== "HTTP" + ``` http + POST /mydownloads HTTP/1.1 + Host: ntfy.sh + Attach: https://f-droid.org/F-Droid.apk + ``` + +=== "JavaScript" + ``` javascript + fetch('https://ntfy.sh/mydownloads', { + method: 'POST', + headers: { 'Attach': 'https://f-droid.org/F-Droid.apk' } + }) + ``` + +=== "Go" + ``` go + req, _ := http.NewRequest("POST", "https://ntfy.sh/mydownloads", file) + req.Header.Set("Attach", "https://f-droid.org/F-Droid.apk") + http.DefaultClient.Do(req) + ``` + +=== "Python" + ``` python + requests.put("https://ntfy.sh/mydownloads", + headers={ "Attach": "https://f-droid.org/F-Droid.apk" }) + ``` + +=== "PHP" + ``` php-inline + file_get_contents('https://ntfy.sh/mydownloads', false, stream_context_create([ + 'http' => [ + 'method' => 'PUT', + 'header' => + "Content-Type: text/plain\r\n" . // Does not matter + "Attach: https://f-droid.org/F-Droid.apk", + ] + ])); + ``` + +
+ ![file attachment](static/img/android-screenshot-attachment-file.png){ width=500 } +
File attachment sent from an external URL
+
-``` ## E-mail notifications You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that @@ -1029,17 +1102,20 @@ parameter (or any of its aliases `unifiedpush` or `up`) to `1` to [disable Fireb option is equivalent to `Firebase: no`, but was introduced to allow future flexibility. ## Limitations -There are a few limitations to the API to prevent abuse and to keep the server healthy. Most of them you won't run into, +There are a few limitations to the API to prevent abuse and to keep the server healthy. Almost all of these settings +are configurable via the server side [rate limiting settings](config.md#rate-limiting). Most of these limits you won't run into, but just in case, let's list them all: -| Limit | Description | -|---|---| -| **Message length** | Each message can be up to 4,096 bytes long. Longer messages are truncated. | -| **Requests** | By default, the server is configured to allow 60 requests at once, and then refills the your allowed requests bucket at a rate of one request per 10 seconds. You can read more about this in the [rate limiting](config.md#rate-limiting) section. | -| **E-mails** | By default, the server is configured to allow sending 16 e-mails at once, and then refills the your allowed e-mail bucket at a rate of one per hour. You can read more about this in the [rate limiting](config.md#rate-limiting) section. | -| **Subscription limits** | By default, the server allows each visitor to keep 30 connections to the server open. | -| **Bandwidth** | By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. | -| **Total number of topics** | By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though. | +| Limit | Description | +|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Message length** | Each message can be up to 4,096 bytes long. Longer messages are treated as [attachments](#attachments). | +| **Requests** | By default, the server is configured to allow 60 requests per visitor at once, and then refills the your allowed requests bucket at a rate of one request per 10 seconds. | +| **E-mails** | By default, the server is configured to allow sending 16 e-mails per visitor at once, and then refills the your allowed e-mail bucket at a rate of one per hour. | +| **Subscription limit** | By default, the server allows each visitor to keep 30 connections to the server open. | +| **Attachment size limit** | By default, the server allows attachments up to 15 MB in size, up to 100 MB in total per visitor and up to 5 GB across all visitors. | +| **Attachment expiry** | By default, the server deletes attachments after 3 hours and thereby frees up space from the total visitor attachment limit. | +| **Attachment bandwidth** | By default, the server allows 500 MB of GET/PUT/POST traffic for attachments per visitor in a 24 hour period. Traffic exceeding that is rejected. | +| **Total number of topics** | By default, the server is configured to allow 15,000 topics. The ntfy.sh server has higher limits though. | ## List of all parameters The following is a list of all parameters that can be passed when publishing a message. Parameter names are **case-insensitive**, @@ -1053,8 +1129,8 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a | `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) | | `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) | | `X-Click` | `Click` | URL to open when [notification is clicked](#click-action) | -| `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments-send-files), as an alternative to PUT/POST-ing an attachment | -| `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments-send-files) filename, as it appears in the client | +| `X-Attach` | `Attach`, `a` | URL to send as an [attachment](#attachments), as an alternative to PUT/POST-ing an attachment | +| `X-Filename` | `Filename`, `file`, `f` | Optional [attachment](#attachments) filename, as it appears in the client | | `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) | | `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) | | `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) | diff --git a/docs/static/img/android-screenshot-attachment-file.png b/docs/static/img/android-screenshot-attachment-file.png new file mode 100644 index 0000000..f151c93 Binary files /dev/null and b/docs/static/img/android-screenshot-attachment-file.png differ diff --git a/docs/static/img/android-screenshot-attachment-image.png b/docs/static/img/android-screenshot-attachment-image.png new file mode 100644 index 0000000..42afba8 Binary files /dev/null and b/docs/static/img/android-screenshot-attachment-image.png differ diff --git a/server/cache_mem.go b/server/cache_mem.go index 04e57be..96c9831 100644 --- a/server/cache_mem.go +++ b/server/cache_mem.go @@ -131,7 +131,8 @@ func (c *memCache) AttachmentsSize(owner string) (int64, error) { var size int64 for topic := range c.messages { for _, m := range c.messages[topic] { - if m.Attachment != nil && m.Attachment.Owner == owner { + counted := m.Attachment != nil && m.Attachment.Owner == owner && m.Attachment.Expires > time.Now().Unix() + if counted { size += m.Attachment.Size } } diff --git a/server/cache_mem_test.go b/server/cache_mem_test.go index 831703a..6e37ab4 100644 --- a/server/cache_mem_test.go +++ b/server/cache_mem_test.go @@ -25,6 +25,10 @@ func TestMemCache_Prune(t *testing.T) { testCachePrune(t, newMemCache()) } +func TestMemCache_Attachments(t *testing.T) { + testCacheAttachments(t, newMemCache()) +} + func TestMemCache_NopCache(t *testing.T) { c := newNopCache() assert.Nil(t, c.AddMessage(newDefaultMessage("mytopic", "my message"))) diff --git a/server/cache_sqlite_test.go b/server/cache_sqlite_test.go index 384da25..a512e6b 100644 --- a/server/cache_sqlite_test.go +++ b/server/cache_sqlite_test.go @@ -29,6 +29,10 @@ func TestSqliteCache_Prune(t *testing.T) { testCachePrune(t, newSqliteTestCache(t)) } +func TestSqliteCache_Attachments(t *testing.T) { + testCacheAttachments(t, newSqliteTestCache(t)) +} + func TestSqliteCache_Migration_From0(t *testing.T) { filename := newSqliteTestCacheFile(t) db, err := sql.Open("sqlite3", filename) diff --git a/server/cache_test.go b/server/cache_test.go index 1eae091..71ba549 100644 --- a/server/cache_test.go +++ b/server/cache_test.go @@ -1,7 +1,7 @@ package server import ( - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "testing" "time" ) @@ -13,71 +13,71 @@ func testCacheMessages(t *testing.T, c cache) { m2 := newDefaultMessage("mytopic", "my other message") m2.Time = 2 - assert.Nil(t, c.AddMessage(m1)) - assert.Nil(t, c.AddMessage(newDefaultMessage("example", "my example message"))) - assert.Nil(t, c.AddMessage(m2)) + require.Nil(t, c.AddMessage(m1)) + require.Nil(t, c.AddMessage(newDefaultMessage("example", "my example message"))) + require.Nil(t, c.AddMessage(m2)) // Adding invalid - assert.Equal(t, errUnexpectedMessageType, c.AddMessage(newKeepaliveMessage("mytopic"))) // These should not be added! - assert.Equal(t, errUnexpectedMessageType, c.AddMessage(newOpenMessage("example"))) // These should not be added! + require.Equal(t, errUnexpectedMessageType, c.AddMessage(newKeepaliveMessage("mytopic"))) // These should not be added! + require.Equal(t, errUnexpectedMessageType, c.AddMessage(newOpenMessage("example"))) // These should not be added! // mytopic: count count, err := c.MessageCount("mytopic") - assert.Nil(t, err) - assert.Equal(t, 2, count) + require.Nil(t, err) + require.Equal(t, 2, count) // mytopic: since all messages, _ := c.Messages("mytopic", sinceAllMessages, false) - assert.Equal(t, 2, len(messages)) - assert.Equal(t, "my message", messages[0].Message) - assert.Equal(t, "mytopic", messages[0].Topic) - assert.Equal(t, messageEvent, messages[0].Event) - assert.Equal(t, "", messages[0].Title) - assert.Equal(t, 0, messages[0].Priority) - assert.Nil(t, messages[0].Tags) - assert.Equal(t, "my other message", messages[1].Message) + require.Equal(t, 2, len(messages)) + require.Equal(t, "my message", messages[0].Message) + require.Equal(t, "mytopic", messages[0].Topic) + require.Equal(t, messageEvent, messages[0].Event) + require.Equal(t, "", messages[0].Title) + require.Equal(t, 0, messages[0].Priority) + require.Nil(t, messages[0].Tags) + require.Equal(t, "my other message", messages[1].Message) // mytopic: since none messages, _ = c.Messages("mytopic", sinceNoMessages, false) - assert.Empty(t, messages) + require.Empty(t, messages) // mytopic: since 2 messages, _ = c.Messages("mytopic", sinceTime(time.Unix(2, 0)), false) - assert.Equal(t, 1, len(messages)) - assert.Equal(t, "my other message", messages[0].Message) + require.Equal(t, 1, len(messages)) + require.Equal(t, "my other message", messages[0].Message) // example: count count, err = c.MessageCount("example") - assert.Nil(t, err) - assert.Equal(t, 1, count) + require.Nil(t, err) + require.Equal(t, 1, count) // example: since all messages, _ = c.Messages("example", sinceAllMessages, false) - assert.Equal(t, "my example message", messages[0].Message) + require.Equal(t, "my example message", messages[0].Message) // non-existing: count count, err = c.MessageCount("doesnotexist") - assert.Nil(t, err) - assert.Equal(t, 0, count) + require.Nil(t, err) + require.Equal(t, 0, count) // non-existing: since all messages, _ = c.Messages("doesnotexist", sinceAllMessages, false) - assert.Empty(t, messages) + require.Empty(t, messages) } func testCacheTopics(t *testing.T, c cache) { - assert.Nil(t, c.AddMessage(newDefaultMessage("topic1", "my example message"))) - assert.Nil(t, c.AddMessage(newDefaultMessage("topic2", "message 1"))) - assert.Nil(t, c.AddMessage(newDefaultMessage("topic2", "message 2"))) - assert.Nil(t, c.AddMessage(newDefaultMessage("topic2", "message 3"))) + require.Nil(t, c.AddMessage(newDefaultMessage("topic1", "my example message"))) + require.Nil(t, c.AddMessage(newDefaultMessage("topic2", "message 1"))) + require.Nil(t, c.AddMessage(newDefaultMessage("topic2", "message 2"))) + require.Nil(t, c.AddMessage(newDefaultMessage("topic2", "message 3"))) topics, err := c.Topics() if err != nil { t.Fatal(err) } - assert.Equal(t, 2, len(topics)) - assert.Equal(t, "topic1", topics["topic1"].ID) - assert.Equal(t, "topic2", topics["topic2"].ID) + require.Equal(t, 2, len(topics)) + require.Equal(t, "topic1", topics["topic1"].ID) + require.Equal(t, "topic2", topics["topic2"].ID) } func testCachePrune(t *testing.T, c cache) { @@ -90,23 +90,23 @@ func testCachePrune(t *testing.T, c cache) { m3 := newDefaultMessage("another_topic", "and another one") m3.Time = 1 - assert.Nil(t, c.AddMessage(m1)) - assert.Nil(t, c.AddMessage(m2)) - assert.Nil(t, c.AddMessage(m3)) - assert.Nil(t, c.Prune(time.Unix(2, 0))) + require.Nil(t, c.AddMessage(m1)) + require.Nil(t, c.AddMessage(m2)) + require.Nil(t, c.AddMessage(m3)) + require.Nil(t, c.Prune(time.Unix(2, 0))) count, err := c.MessageCount("mytopic") - assert.Nil(t, err) - assert.Equal(t, 1, count) + require.Nil(t, err) + require.Equal(t, 1, count) count, err = c.MessageCount("another_topic") - assert.Nil(t, err) - assert.Equal(t, 0, count) + require.Nil(t, err) + require.Equal(t, 0, count) messages, err := c.Messages("mytopic", sinceAllMessages, false) - assert.Nil(t, err) - assert.Equal(t, 1, len(messages)) - assert.Equal(t, "my other message", messages[0].Message) + require.Nil(t, err) + require.Equal(t, 1, len(messages)) + require.Equal(t, "my other message", messages[0].Message) } func testCacheMessagesTagsPrioAndTitle(t *testing.T, c cache) { @@ -114,12 +114,12 @@ func testCacheMessagesTagsPrioAndTitle(t *testing.T, c cache) { m.Tags = []string{"tag1", "tag2"} m.Priority = 5 m.Title = "some title" - assert.Nil(t, c.AddMessage(m)) + require.Nil(t, c.AddMessage(m)) messages, _ := c.Messages("mytopic", sinceAllMessages, false) - assert.Equal(t, []string{"tag1", "tag2"}, messages[0].Tags) - assert.Equal(t, 5, messages[0].Priority) - assert.Equal(t, "some title", messages[0].Title) + require.Equal(t, []string{"tag1", "tag2"}, messages[0].Tags) + require.Equal(t, 5, messages[0].Priority) + require.Equal(t, "some title", messages[0].Title) } func testCacheMessagesScheduled(t *testing.T, c cache) { @@ -130,20 +130,93 @@ func testCacheMessagesScheduled(t *testing.T, c cache) { m3.Time = time.Now().Add(time.Minute).Unix() // earlier than m2! m4 := newDefaultMessage("mytopic2", "message 4") m4.Time = time.Now().Add(time.Minute).Unix() - assert.Nil(t, c.AddMessage(m1)) - assert.Nil(t, c.AddMessage(m2)) - assert.Nil(t, c.AddMessage(m3)) + require.Nil(t, c.AddMessage(m1)) + require.Nil(t, c.AddMessage(m2)) + require.Nil(t, c.AddMessage(m3)) messages, _ := c.Messages("mytopic", sinceAllMessages, false) // exclude scheduled - assert.Equal(t, 1, len(messages)) - assert.Equal(t, "message 1", messages[0].Message) + require.Equal(t, 1, len(messages)) + require.Equal(t, "message 1", messages[0].Message) messages, _ = c.Messages("mytopic", sinceAllMessages, true) // include scheduled - assert.Equal(t, 3, len(messages)) - assert.Equal(t, "message 1", messages[0].Message) - assert.Equal(t, "message 3", messages[1].Message) // Order! - assert.Equal(t, "message 2", messages[2].Message) + require.Equal(t, 3, len(messages)) + require.Equal(t, "message 1", messages[0].Message) + require.Equal(t, "message 3", messages[1].Message) // Order! + require.Equal(t, "message 2", messages[2].Message) messages, _ = c.MessagesDue() - assert.Empty(t, messages) + require.Empty(t, messages) +} + +func testCacheAttachments(t *testing.T, c cache) { + expires1 := time.Now().Add(-4 * time.Hour).Unix() + m := newDefaultMessage("mytopic", "flower for you") + m.ID = "m1" + m.Attachment = &attachment{ + Name: "flower.jpg", + Type: "image/jpeg", + Size: 5000, + Expires: expires1, + URL: "https://ntfy.sh/file/AbDeFgJhal.jpg", + Owner: "1.2.3.4", + } + require.Nil(t, c.AddMessage(m)) + + expires2 := time.Now().Add(2 * time.Hour).Unix() // Future + m = newDefaultMessage("mytopic", "sending you a car") + m.ID = "m2" + m.Attachment = &attachment{ + Name: "car.jpg", + Type: "image/jpeg", + Size: 10000, + Expires: expires2, + URL: "https://ntfy.sh/file/aCaRURL.jpg", + Owner: "1.2.3.4", + } + require.Nil(t, c.AddMessage(m)) + + expires3 := time.Now().Add(1 * time.Hour).Unix() // Future + m = newDefaultMessage("another-topic", "sending you another car") + m.ID = "m3" + m.Attachment = &attachment{ + Name: "another-car.jpg", + Type: "image/jpeg", + Size: 20000, + Expires: expires3, + URL: "https://ntfy.sh/file/zakaDHFW.jpg", + Owner: "1.2.3.4", + } + require.Nil(t, c.AddMessage(m)) + + messages, err := c.Messages("mytopic", sinceAllMessages, false) + require.Nil(t, err) + require.Equal(t, 2, len(messages)) + + require.Equal(t, "flower for you", messages[0].Message) + require.Equal(t, "flower.jpg", messages[0].Attachment.Name) + require.Equal(t, "image/jpeg", messages[0].Attachment.Type) + require.Equal(t, int64(5000), messages[0].Attachment.Size) + require.Equal(t, expires1, messages[0].Attachment.Expires) + require.Equal(t, "https://ntfy.sh/file/AbDeFgJhal.jpg", messages[0].Attachment.URL) + require.Equal(t, "1.2.3.4", messages[0].Attachment.Owner) + + require.Equal(t, "sending you a car", messages[1].Message) + require.Equal(t, "car.jpg", messages[1].Attachment.Name) + require.Equal(t, "image/jpeg", messages[1].Attachment.Type) + require.Equal(t, int64(10000), messages[1].Attachment.Size) + require.Equal(t, expires2, messages[1].Attachment.Expires) + require.Equal(t, "https://ntfy.sh/file/aCaRURL.jpg", messages[1].Attachment.URL) + require.Equal(t, "1.2.3.4", messages[1].Attachment.Owner) + + size, err := c.AttachmentsSize("1.2.3.4") + require.Nil(t, err) + require.Equal(t, int64(30000), size) + + size, err = c.AttachmentsSize("5.6.7.8") + require.Nil(t, err) + require.Equal(t, int64(0), size) + + ids, err := c.AttachmentsExpired() + require.Nil(t, err) + require.Equal(t, []string{"m1"}, ids) } diff --git a/server/server.yml b/server/server.yml index a00acc2..d165047 100644 --- a/server/server.yml +++ b/server/server.yml @@ -36,7 +36,7 @@ # # You can disable the cache entirely by setting this to 0. # -# cache-duration: 12h +# cache-duration: "12h" # If set, the X-Forwarded-For header is used to determine the visitor IP address # instead of the remote address of the connection. @@ -46,6 +46,19 @@ # # behind-proxy: false +# If enabled, clients can attach files to notifications as attachments. Minimum settings to enable attachments +# are "attachment-cache-dir" and "base-url". +# +# - attachment-cache-dir is the cache directory for attached files +# - attachment-total-size-limit is the limit of the on-disk attachment cache directory (total size) +# - attachment-file-size-limit is the per-file attachment size limit (e.g. 300k, 2M, 100M) +# - attachment-expiry-duration is the duration after which uploaded attachments will be deleted (e.g. 3h, 20h) +# +# attachment-cache-dir: +# attachment-total-size-limit: "5G" +# attachment-file-size-limit: "15M" +# attachment-expiry-duration: "3h" + # If enabled, allow outgoing e-mail notifications via the 'X-Email' header. If this header is set, # messages will additionally be sent out as e-mail using an external SMTP server. As of today, only # SMTP servers with plain text auth and STARTLS are supported. Please also refer to the rate limiting settings @@ -78,12 +91,12 @@ # # Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. # -# keepalive-interval: 30s +# keepalive-interval: "30s" # Interval in which the manager prunes old messages, deletes topics # and prints the stats. # -# manager-interval: 1m +# manager-interval: "1m" # Rate limiting: Total number of topics before the server rejects new topics. # @@ -98,11 +111,18 @@ # - visitor-request-limit-replenish is the rate at which the bucket is refilled # # visitor-request-limit-burst: 60 -# visitor-request-limit-replenish: 10s +# visitor-request-limit-replenish: "10s" # Rate limiting: Allowed emails per visitor: # - visitor-email-limit-burst is the initial bucket of emails each visitor has # - visitor-email-limit-replenish is the rate at which the bucket is refilled # # visitor-email-limit-burst: 16 -# visitor-email-limit-replenish: 1h +# visitor-email-limit-replenish: "1h" + +# Rate limiting: Attachment size and bandwidth limits per visitor: +# - visitor-attachment-total-size-limit is the total storage limit used for attachments per visitor +# - visitor-attachment-daily-bandwidth-limit is the total daily attachment download/upload traffic limit per visitor +# +# visitor-attachment-total-size-limit: "100M" +# visitor-attachment-daily-bandwidth-limit: "500M" diff --git a/server/server_test.go b/server/server_test.go index 04ac558..4867e4a 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -699,12 +699,21 @@ func TestServer_PublishAttachment(t *testing.T) { require.Equal(t, 200, response.Code) require.Equal(t, "5000", response.Header().Get("Content-Length")) require.Equal(t, content, response.Body.String()) + + // Slightly unrelated cross-test: make sure we add an owner for internal attachments + size, err := s.cache.AttachmentsSize("9.9.9.9") // See request() + require.Nil(t, err) + require.Equal(t, int64(5000), size) } func TestServer_PublishAttachmentShortWithFilename(t *testing.T) { - s := newTestServer(t, newTestConfig(t)) + c := newTestConfig(t) + c.BehindProxy = true + s := newTestServer(t, c) content := "this is an ATTACHMENT" - response := request(t, s, "PUT", "/mytopic?f=myfile.txt", content, nil) + response := request(t, s, "PUT", "/mytopic?f=myfile.txt", content, map[string]string{ + "X-Forwarded-For": "1.2.3.4", + }) msg := toMessage(t, response.Body.String()) require.Equal(t, "myfile.txt", msg.Attachment.Name) require.Equal(t, "text/plain; charset=utf-8", msg.Attachment.Type) @@ -719,6 +728,11 @@ func TestServer_PublishAttachmentShortWithFilename(t *testing.T) { require.Equal(t, 200, response.Code) require.Equal(t, "21", response.Header().Get("Content-Length")) require.Equal(t, content, response.Body.String()) + + // Slightly unrelated cross-test: make sure we add an owner for internal attachments + size, err := s.cache.AttachmentsSize("1.2.3.4") + require.Nil(t, err) + require.Equal(t, int64(21), size) } func TestServer_PublishAttachmentExternalWithoutFilename(t *testing.T) { @@ -734,6 +748,11 @@ func TestServer_PublishAttachmentExternalWithoutFilename(t *testing.T) { require.Equal(t, int64(0), msg.Attachment.Expires) require.Equal(t, "https://upload.wikimedia.org/wikipedia/commons/f/fd/Pink_flower.jpg", msg.Attachment.URL) require.Equal(t, "", msg.Attachment.Owner) + + // Slightly unrelated cross-test: make sure we don't add an owner for external attachments + size, err := s.cache.AttachmentsSize("127.0.0.1") + require.Nil(t, err) + require.Equal(t, int64(0), size) } func TestServer_PublishAttachmentExternalWithFilename(t *testing.T) { @@ -914,6 +933,7 @@ func request(t *testing.T, s *Server, method, url, body string, headers map[stri if err != nil { t.Fatal(err) } + req.RemoteAddr = "9.9.9.9" // Used for tests for k, v := range headers { req.Header.Set(k, v) }