mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-09-12 22:25:49 -04:00
Merge pull request #1695 from justadityaraj/feat/ntfy-custom-query-string
Add custom header and URL query string options to the ntfy publisher
This commit is contained in:
3 files changed
+158
-3
No files matched your search
@@ -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.
|
||||
@@ -555,6 +555,78 @@
|
||||
"string": "Enable TLS support. Disable if you are using a self-signed certificate."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "URL_QUERY_STRING",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [{ "type": "password" }], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "URL query string"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Optional query string appended to the ntfy request URL (e.g. <code>p_token=tokenId.tokenValue</code>). Useful when ntfy is behind a reverse proxy or tunnel (Pangolin, Tailscale, ...) that authenticates via a query parameter. Note: values in the URL may be recorded in proxy/server access logs, so for secrets prefer the custom header below. Leave empty to disable."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "CUSTOMHEADER_NAME",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Custom header name"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Optional custom HTTP header name sent with the ntfy request, e.g. to authenticate through a reverse proxy or tunnel. Requires the custom header value to also be set. Leave empty to disable."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "CUSTOMHEADER_VALUE",
|
||||
"type": {
|
||||
"dataType": "string",
|
||||
"elements": [
|
||||
{ "elementType": "input", "elementOptions": [{ "type": "password" }], "transformers": [] }
|
||||
]
|
||||
},
|
||||
"default_value": "",
|
||||
"options": [],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Custom header value"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Value for the custom HTTP header defined above. Requires the custom header name to also be set. Leave empty to disable."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import requests
|
||||
from base64 import b64encode
|
||||
@@ -94,6 +95,11 @@ def send(html, text):
|
||||
user = get_setting_value('NTFY_USER')
|
||||
pwd = get_setting_value('NTFY_PASSWORD')
|
||||
verify_ssl = get_setting_value('NTFY_VERIFY_SSL')
|
||||
custom_header_name = get_setting_value('NTFY_CUSTOMHEADER_NAME')
|
||||
custom_header_value = get_setting_value('NTFY_CUSTOMHEADER_VALUE')
|
||||
# Strip a leading '?' so both "p_token=..." and "?p_token=..." work; requests
|
||||
# adds the '?' itself, and a leading one would produce a broken "??" in the URL.
|
||||
url_query_string = get_setting_value('NTFY_URL_QUERY_STRING').lstrip('?')
|
||||
|
||||
# prepare request headers
|
||||
headers = {
|
||||
@@ -112,6 +118,17 @@ def send(html, text):
|
||||
# add authorization header with hash
|
||||
headers["Authorization"] = "Basic {}".format(basichash)
|
||||
|
||||
# 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:
|
||||
response = requests.post("{}/{}".format(
|
||||
@@ -119,6 +136,7 @@ def send(html, text):
|
||||
get_setting_value('NTFY_TOPIC')),
|
||||
data = text,
|
||||
headers = headers,
|
||||
params = url_query_string if url_query_string != '' else None,
|
||||
verify = verify_ssl,
|
||||
timeout = get_setting_value('NTFY_RUN_TIMEOUT')
|
||||
)
|
||||
@@ -131,10 +149,35 @@ def send(html, text):
|
||||
else:
|
||||
response_text = json.dumps(response.text)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
mylog('none', [f'[{pluginName}] ⚠ ERROR: ', e])
|
||||
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.')
|
||||
|
||||
response_text = e
|
||||
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
|
||||
# logged and persisted to the plugin result file / shown in the UI.
|
||||
error_text = str(e)
|
||||
if url_query_string != '':
|
||||
error_text = re.sub(r'(\?)\S+', r'\1<redacted>', error_text)
|
||||
|
||||
mylog('none', [f'[{pluginName}] ⚠ ERROR: ', error_text])
|
||||
|
||||
response_text = error_text
|
||||
|
||||
return response_text, response_status_code
|
||||
|
||||
|
||||
Reference in new issue
Block a user