Apprise URLs: ntfy://topic, the form's own example, is refused on a default instance because the topic is checked as a hostname #16

Closed
opened 2026-10-01 10:48:17 +00:00 by tiagoagueda · 1 comment
Owner

Found on 2026-09-29 in a read-only review of postulo-apprise at 7637d899d, running the help text's own examples through validate() with Apprise 1.13.1 and a stub resolver.

Related to #1.

names_a_server() answers per class: does the URL template declare a host token. Several services declare one and keep something other than the dialled address there. The value is turned into https://<value> and handed to check_destination, which refuses a name that does not resolve, so a connection to an entirely public service is refused with a sentence about private addresses. The docstring says this costs one scheme, wechat; it costs at least six, and one of them is the example printed under the field.

  • Where: src/postulo_apprise/notifier.py:96-99 (names_a_server: return "host" in template); :110-120 (destination_of); :184-187 (the refusal sentence, which prints server.host); :144-145 (the help text: "tgram://bot-token/chat-id, ntfy://topic, …"); :91-94 (the docstring: "What that costs, in this Apprise, is wechat"). In Apprise 1.13.1: plugins/ntfy.py:980-998 chooses cloud mode when the host slot is not followed by a topic and keeps the topic in host, and :533-535 then sends to cloud_notify_url (https://ntfy.sh, :164); plugins/mailgun.py:140-141, plugins/sparkpost.py:163-164, plugins/smtp2go.py:112-113 and plugins/sendpulse.py:114-115 declare host as the sender's "Domain" and send to their own API.
  • What happens: on an instance with the default policy, a person types the help text's example, ntfy://my-topic. Checked on the snapshot: Apprise reports mode='cloud', host='my-topic'; destination_of returns https://my-topic; the form answers "ntfy at my-topic: That hostname could not be resolved. Connections may only reach private or local addresses when the operator sets POSTULO_CONNECTIONS_ALLOW_PRIVATE=true." A Mailgun line is refused the same way when its sending domain has no address record, which is usual for a subdomain that carries only MX and TXT records; so are SparkPost, SMTP2GO and SendPulse lines. ntfy://ntfy.sh/my-topic is accepted, but nothing on the form suggests it. A topic that happens to match a name the resolver knows (a Compose service, say) is refused or accepted for reasons that have nothing to do with ntfy.
  • Why it matters: the most common ntfy set-up cannot be saved as the form describes it, and the person is told to ask the operator to allow private destinations for a service that is public. The sentence also names server.host as if it were an address; for cloud ntfy that is the topic, which is all that protects a topic on ntfy.sh, and when the same refusal is raised at send time (_client, :190-195) it is stored on the connection and written to the log by the core (notifications/service.py:86-89). No test covers a cloud-mode line: the tests use ntfy://ntfy.example.org/postulo and ntfy://internal/postulo only.
  • Fix: decide per server object, not per class.
    • For ntfy, use the server's mode: in cloud mode the destination is ntfy.sh (or none), not host.
    • For Mailgun, SparkPost, SMTP2GO, SendPulse and WeChat, treat host as not dialled.
    • This is the same per-scheme accessor that the unchecked smtp= and keyfile= arguments need (filed alongside this issue), so one table of reviewed schemes with "what does this server dial" answers both.
    • In the refusal, name the line number, as rejected lines already do, rather than server.host, unless the value is known to be an address.
    • Correct the docstring's count.

Done when

  • A test in tests/test_apprise.py, with POSTULO_CONNECTIONS_ALLOW_PRIVATE off and a resolver that knows no single-label name, shows validate() accepting every example in the field's help text, ntfy://topic included, and a Mailgun line whose domain does not resolve.
  • The same test shows that ntfy://internal/postulo (a server of the person's own) is still checked and refused.
Found on 2026-09-29 in a read-only review of postulo-apprise at 7637d899d, running the help text's own examples through `validate()` with Apprise 1.13.1 and a stub resolver. Related to #1. `names_a_server()` answers per class: does the URL template declare a `host` token. Several services declare one and keep something other than the dialled address there. The value is turned into `https://<value>` and handed to `check_destination`, which refuses a name that does not resolve, so a connection to an entirely public service is refused with a sentence about private addresses. The docstring says this costs one scheme, `wechat`; it costs at least six, and one of them is the example printed under the field. - **Where:** `src/postulo_apprise/notifier.py:96-99` (`names_a_server`: `return "host" in template`); `:110-120` (`destination_of`); `:184-187` (the refusal sentence, which prints `server.host`); `:144-145` (the help text: "tgram://bot-token/chat-id, ntfy://topic, …"); `:91-94` (the docstring: "What that costs, in this Apprise, is ``wechat``"). In Apprise 1.13.1: `plugins/ntfy.py:980-998` chooses cloud mode when the host slot is not followed by a topic and keeps the topic in `host`, and `:533-535` then sends to `cloud_notify_url` (`https://ntfy.sh`, `:164`); `plugins/mailgun.py:140-141`, `plugins/sparkpost.py:163-164`, `plugins/smtp2go.py:112-113` and `plugins/sendpulse.py:114-115` declare `host` as the sender's "Domain" and send to their own API. - **What happens:** on an instance with the default policy, a person types the help text's example, `ntfy://my-topic`. Checked on the snapshot: Apprise reports `mode='cloud'`, `host='my-topic'`; `destination_of` returns `https://my-topic`; the form answers "ntfy at my-topic: That hostname could not be resolved. Connections may only reach private or local addresses when the operator sets POSTULO_CONNECTIONS_ALLOW_PRIVATE=true." A Mailgun line is refused the same way when its sending domain has no address record, which is usual for a subdomain that carries only MX and TXT records; so are SparkPost, SMTP2GO and SendPulse lines. `ntfy://ntfy.sh/my-topic` is accepted, but nothing on the form suggests it. A topic that happens to match a name the resolver knows (a Compose service, say) is refused or accepted for reasons that have nothing to do with ntfy. - **Why it matters:** the most common ntfy set-up cannot be saved as the form describes it, and the person is told to ask the operator to allow private destinations for a service that is public. The sentence also names `server.host` as if it were an address; for cloud ntfy that is the topic, which is all that protects a topic on ntfy.sh, and when the same refusal is raised at send time (`_client`, `:190-195`) it is stored on the connection and written to the log by the core (`notifications/service.py:86-89`). No test covers a cloud-mode line: the tests use `ntfy://ntfy.example.org/postulo` and `ntfy://internal/postulo` only. - **Fix:** decide per server object, not per class. - For ntfy, use the server's `mode`: in cloud mode the destination is `ntfy.sh` (or none), not `host`. - For Mailgun, SparkPost, SMTP2GO, SendPulse and WeChat, treat `host` as not dialled. - This is the same per-scheme accessor that the unchecked `smtp=` and `keyfile=` arguments need (filed alongside this issue), so one table of reviewed schemes with "what does this server dial" answers both. - In the refusal, name the line number, as rejected lines already do, rather than `server.host`, unless the value is known to be an address. - Correct the docstring's count. ## Done when - A test in `tests/test_apprise.py`, with `POSTULO_CONNECTIONS_ALLOW_PRIVATE` off and a resolver that knows no single-label name, shows `validate()` accepting every example in the field's help text, `ntfy://topic` included, and a Mailgun line whose domain does not resolve. - The same test shows that `ntfy://internal/postulo` (a server of the person's own) is still checked and refused.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:48:17 +00:00
Author
Owner

Fixed by pull request #23.

Fixed by pull request #23.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo-apprise#16
No description provided.