From 93955666d491512515cf67fa87db64039dec614b Mon Sep 17 00:00:00 2001 From: Aditya Raj Singh Date: Wed, 1 Jul 2026 21:55:15 +0530 Subject: [PATCH] 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. --- front/plugins/_publisher_ntfy/config.json | 72 +++++++++++++++++++++++ front/plugins/_publisher_ntfy/ntfy.py | 27 ++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/front/plugins/_publisher_ntfy/config.json b/front/plugins/_publisher_ntfy/config.json index 16a37048a..6c4560b18 100755 --- a/front/plugins/_publisher_ntfy/config.json +++ b/front/plugins/_publisher_ntfy/config.json @@ -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. p_token=tokenId.tokenValue). 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." + } + ] } ] } diff --git a/front/plugins/_publisher_ntfy/ntfy.py b/front/plugins/_publisher_ntfy/ntfy.py index 97444bea1..99a7f6650 100755 --- a/front/plugins/_publisher_ntfy/ntfy.py +++ b/front/plugins/_publisher_ntfy/ntfy.py @@ -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', error_text) + + mylog('none', [f'[{pluginName}] ⚠ ERROR: ', error_text]) - response_text = e + response_text = error_text return response_text, response_status_code