-
-
Notifications
You must be signed in to change notification settings - Fork 425
Add custom header and URL query string options to the ntfy publisher #1695
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -555,6 +555,78 @@ | |
| "string": "Enable TLS support. Disable if you are using a self-signed certificate." | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "function": "URL_QUERY_STRING", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's add sample values into the README.md in case people are confused how a valid value should look like |
||
| "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." | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,13 +118,25 @@ 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 | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| custom_header_applied = True | ||
|
|
||
| # call NTFY service | ||
| try: | ||
| response = requests.post("{}/{}".format( | ||
| get_setting_value('NTFY_HOST'), | ||
| get_setting_value('NTFY_TOPIC')), | ||
| data = text, | ||
| headers = headers, | ||
| params = url_query_string if url_query_string != '' else None, | ||
|
Comment on lines
132
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π Security & Privacy | π Major | β‘ Quick win Redact query strings from non-200 response bodies. Line 139 sends the configured query string, but only Redact query strings from Also applies to: 170-180 π€ Prompt for AI Agents |
||
| 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.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: | ||
| 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) | ||
|
|
||
| mylog('none', [f'[{pluginName}] β ERROR: ', error_text]) | ||
|
|
||
| response_text = e | ||
| response_text = error_text | ||
|
|
||
| return response_text, response_status_code | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π― Functional Correctness | π‘ Minor | β‘ Quick win
π§© Analysis chain
π Script executed:
Repository: netalertx/NetAlertX
Length of output: 27372
π Script executed:
Repository: netalertx/NetAlertX
Length of output: 3144
π Web query:
Cloudflare Access read_service_tokens_from_header compact JSON CF-Access-Client-Id CF-Access-Client-Secretπ‘ Result:
In Cloudflare Access, the read_service_tokens_from_header configuration allows you to authenticate service-to-service requests using a single custom HTTP header instead of the default CF-Access-Client-Id and CF-Access-Client-Secret header pair [1][2]. This is particularly useful for integrating with SaaS services or legacy systems that only permit the inclusion of a single custom header, such as Authorization [1][2]. When this feature is enabled, the value of the specified header must be a JSON object containing the Client ID and Client Secret [3][4]. Example of the expected header format: Authorization: {"cf-access-client-id": "<CLIENT_ID>", "cf-access-client-secret": "<CLIENT_SECRET>"} To configure an Access application to use this method, you must update the application configuration via the Cloudflare API, setting the read_service_tokens_from_header field to the name of the desired header (e.g., "Authorization") [1][2]. It is important to note that when updating this configuration, you should perform a GET request first to retrieve the current application settings and include all existing fields in your PUT request to avoid accidentally overwriting other configurations [1][2]. Standard behavior for Cloudflare Access remains the use of the two distinct headers CF-Access-Client-Id and CF-Access-Client-Secret if this single-header option is not configured [5][6][7].
Citations:
Correct the Cloudflare Access example.
Configure
read_service_tokens_from_headerfor the selected header and use a compact JSON value containingcf-access-client-idandcf-access-client-secret. Without this configuration, Cloudflare requires both standard headers, but the plugin sends only one custom header. Otherwise, remove this example.π€ Prompt for AI Agents
Source: MCP tools