Add custom header and URL query string options to the ntfy publisher

Lets users authenticate ntfy notifications through a reverse proxy or tunnel
(Pangolin, Tailscale, ...) in front of the ntfy instance. Adds three optional,
backward-compatible settings that default to empty and are no-ops when unset:

- NTFY_URL_QUERY_STRING: appended to the request URL (e.g. p_token=...). A
  leading '?' is tolerated, and the value is redacted from error logs / the
  plugin result file since the request URL can carry a secret token.
- NTFY_CUSTOMHEADER_NAME / NTFY_CUSTOMHEADER_VALUE: a custom request header,
  skipped with a warning if it would clobber a built-in header (e.g.
  Authorization) so ntfy's own auth stays intact.

Secret-bearing fields are password-masked in the UI. Addresses #1663.
This commit is contained in:
Aditya Raj Singh committed 2026-08-17 01:24:33 +05:30
1 parent 67bad01aca
commit a297bf18c6
2 files changed
+97 -2

No files matched your search

@@ -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."
}
]
}
]
}
+25 -2
View File
@@ -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,15 @@ 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.
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
# call NTFY service
try:
response = requests.post("{}/{}".format(
@@ -119,6 +134,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')
)
@@ -132,9 +148,16 @@ def send(html, text):
response_text = json.dumps(response.text)
except requests.exceptions.RequestException as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR: ', 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)
response_text = e
mylog('none', [f'[{pluginName}] ⚠ ERROR: ', error_text])
response_text = error_text
return response_text, response_status_code