diff --git a/server/plugins/_publisher_ntfy/README.md b/server/plugins/_publisher_ntfy/README.md index 58e14658..d86fa4d8 100755 --- a/server/plugins/_publisher_ntfy/README.md +++ b/server/plugins/_publisher_ntfy/README.md @@ -6,3 +6,43 @@ A plugin to publish a notification via the NTFY gateway. Enable sending notifica - Go to settings and fill in relevant details. +## Reverse proxy / tunnel authentication + +If your ntfy instance sits behind a reverse proxy or tunnel that authenticates requests itself (Pangolin, Tailscale, Cloudflare Access, ...), the proxy usually expects its own credential *in addition to* any ntfy token. Two optional settings cover this. + +Both are independent of `NTFY_TOKEN` / `NTFY_USER` / `NTFY_PASSWORD` — those still control authentication against ntfy itself and are unaffected. + +### Custom header + +Sends an extra HTTP header with the request. Prefer this over the query string for anything secret. + +| Setting | Sample value | +|---|---| +| `NTFY_CUSTOMHEADER_NAME` | `X-Proxy-Token` | +| `NTFY_CUSTOMHEADER_VALUE` | `p_abc123.def456ghi789` | + +Other common examples: + +| Proxy | Header name | Header value | +|---|---|---| +| Pangolin | `P-Token` | `tokenId.tokenValue` | +| Cloudflare Access | `CF-Access-Client-Id` | `abc123.access` | +| Generic bearer gateway | `X-Auth-Token` | `eyJhbGciOi...` | + +Both settings must be filled in — setting only one of them does nothing. + +The header value must be a valid HTTP header value: plain ASCII, no newlines, and no leading or trailing whitespace. A trailing newline pasted in from a text file is the most common mistake and the plugin will report it as an invalid custom header. + +If the header name collides with one the plugin has already set for this request (`Title`, `Actions`, `Priority`, `Tags`, plus `Authorization` when an ntfy token or username/password is configured), the custom header is skipped and a warning is logged, so it can never clobber your ntfy credentials. With no ntfy credentials configured there is no `Authorization` header to clash with, so you are free to use that name for the proxy. + +### URL query string + +Appends a query string to the ntfy request URL, for proxies that authenticate via a query parameter instead of a header. + +| Setting | Sample value | +|---|---| +| `NTFY_URL_QUERY_STRING` | `p_token=tokenId.tokenValue` | + +A leading `?` is optional — both `p_token=...` and `?p_token=...` work. Multiple parameters are supported: `p_token=abc&source=netalertx`. + +Note that query strings are commonly recorded in proxy and web-server access logs, so for secrets the custom header above is the safer option. The plugin redacts the query string from any error message it logs. diff --git a/server/plugins/_publisher_ntfy/ntfy.py b/server/plugins/_publisher_ntfy/ntfy.py index bb586685..f9a9b720 100755 --- a/server/plugins/_publisher_ntfy/ntfy.py +++ b/server/plugins/_publisher_ntfy/ntfy.py @@ -121,11 +121,13 @@ def send(html, text): # Optional custom header, e.g. to authenticate through a reverse proxy / tunnel # (Pangolin, Tailscale, ...) sitting in front of the ntfy instance. Skip it if it # would clobber a built-in header (e.g. Authorization) so ntfy auth stays intact. + custom_header_applied = False if custom_header_name != '' and custom_header_value != '': if custom_header_name.lower() in {k.lower() for k in headers}: mylog('none', [f'[{pluginName}] ⚠ Custom header "{custom_header_name}" collides with a built-in header; skipping it.']) else: headers[custom_header_name] = custom_header_value + custom_header_applied = True # call NTFY service try: @@ -147,6 +149,24 @@ def send(html, text): else: response_text = json.dumps(response.text) + except requests.exceptions.InvalidHeader: + # requests echoes the offending header value in this exception's message, + # so the message itself is never logged - it would leak the configured + # custom header value. Report the problem without quoting the value. + if custom_header_applied: + error_text = (f'Invalid custom header "{custom_header_name}" - the header name or value contains ' + f'characters that are not allowed in an HTTP header (e.g. a newline, a leading space, ' + f'or a non-ASCII character). Check for trailing whitespace on the value.') + else: + error_text = ('A request header contains characters that are not allowed in an HTTP header. Check the ' + 'NTFY_* settings for stray newlines or non-ASCII characters.') + + mylog('none', [f'[{pluginName}] ⚠ ERROR: ', error_text]) + + response_text = error_text + + return response_text, response_status_code + except requests.exceptions.RequestException as e: # The exception message embeds the request URL, which may include a secret # query string (e.g. a proxy token). Redact the query part before it is