This server is also a WebSub hub. WebSub (formerly PubSubHubbub) is a W3C-standard publish/subscribe protocol for web content: subscribers register a callback, the hub verifies their intent, and on each update the hub delivers the full feed body to the callback — optionally signed.
A single publish reaches WebSub subscribers and rssCloud subscribers alike; see How it fits together.
All hub operations share one endpoint:
POST /websub — application/x-www-form-urlencoded, with hub.* fields. (The
path is configurable via WEBSUB_PATH; /websub is the default.)
| Field | Required | Meaning |
|---|---|---|
hub.mode |
yes | subscribe or unsubscribe. |
hub.callback |
yes | Absolute URL the hub delivers to (and verifies against). |
hub.topic |
yes | Absolute URL of the feed you want. |
hub.lease_seconds |
no | Requested subscription lifetime; the hub clamps it (see Leases). |
hub.secret |
no | Shared secret that opts you into signed delivery. |
A well-formed request is acknowledged immediately with 202 Accepted; a malformed
one (missing/relative hub.callback, empty hub.topic, unknown hub.mode) returns
400. The 202 only means the request was accepted — the subscription is not
active until intent verification succeeds.
After a 202, the hub confirms the request out of band by sending a GET to your
hub.callback with these query parameters:
| Parameter | Meaning |
|---|---|
hub.mode |
subscribe or unsubscribe (echoes the request). |
hub.topic |
The topic URL. |
hub.challenge |
A random token. |
hub.lease_seconds |
The lease the hub actually granted (subscribe only; see below). |
To confirm, respond 2xx with a body that is exactly the hub.challenge
value. Any other status, or a body that doesn't match, and the hub discards the
subscription.
hub.lease_seconds is a request, not a guarantee. The hub clamps it to its configured
bounds and tells you the granted value in the verification GET's hub.lease_seconds:
| Bound | Config key | Default |
|---|---|---|
| default | WEBSUB_LEASE_DEFAULT_SECS |
86400 (1 day) |
| minimum | WEBSUB_LEASE_MIN_SECS |
300 (5 minutes) |
| maximum | WEBSUB_LEASE_MAX_SECS |
864000 (10 days) |
If you omit hub.lease_seconds you get the default. Re-subscribe before the lease
expires to renew it.
When the topic changes, the hub POSTs to your hub.callback:
Content-Type — relayed from the origin feed (or application/octet-stream if
the origin sent none).Link — <hub-url>; rel="hub", <topic-url>; rel="self", advertising the hub and
the canonical topic.X-Hub-Signature — only when you subscribed with a hub.secret (see below).Respond with any 2xx to acknowledge. Redirects are followed.
If you supplied hub.secret, the hub signs each delivery with an
X-Hub-Signature: <algo>=<hex> header, where <hex> is the HMAC of the request body
keyed by your secret. The algorithm is sha256 by default (configurable via
WEBSUB_SIGNATURE_ALGO). Recompute the HMAC over the received body and compare to
reject spoofed deliveries.
A publisher can notify the hub natively over WebSub instead of an rssCloud ping:
| Field | Required | Meaning |
|---|---|---|
hub.mode |
yes | publish. |
hub.url |
yes* | The topic URL that changed. (hub.topic is accepted as a fallback.) |
The hub answers 202, then re-fetches the topic and fans the change out. This is
exactly the path an rssCloud /ping takes, so a WebSub publish also reaches rssCloud
subscribers — see How it fits together.
Both hub.topic/hub.url (which the hub fetches) and hub.callback (which the hub
delivers the fetched body to) are supplied by untrusted clients. To stop them being
pointed at the hub's own network, every outbound request — topic re-fetch, the intent
verification GET, and content delivery — is screened: the destination is rejected at
connect time if its host resolves to a non-public address (loopback, private, link-local
incl. cloud-metadata 169.254.169.254, unique-local, CGNAT). Screening is done on the
resolved IP and re-applied on every redirect hop, so a hostname or redirect that points
inward is refused, not followed.
The guard is always on — there is no disable switch. For a loopback or private test
target, allowlist the range (e.g. 127.0.0.0/8) on the path(s) below rather than turning
screening off.
The exemption allowlist is split by trust so a trusted-feed exemption can't be turned
into a callback-SSRF: the topic-fetch path and the callback path (delivery + verification
GET, both to attacker-supplied hub.callback URLs) have separate allowlists. Exempting a
private range for your feeds does not let an attacker register a hub.callback in that
range.
| Config key | Default | Meaning |
|---|---|---|
WEBSUB_FETCH_ALLOW_CIDRS |
(none) | Comma-separated CIDRs exempted on the topic-fetch path only, for a hub that fetches feeds on a private LAN (e.g. 10.0.0.0/8,192.168.0.0/16). |
WEBSUB_CALLBACK_ALLOW_CIDRS |
(none) | Comma-separated CIDRs exempted on the callback path (delivery + verification), for a hub with genuine subscribers on a private LAN. Strict by default. |
A blocked request surfaces as a failed fetch: the topic re-fetch reports a read failure and a blocked callback counts as a failed delivery.
So that subscribers can discover this hub, advertise it from the resource you want watched. Per the WebSub spec, advertise it two ways, in priority order:
HTTP Link header (primary). When your server returns the feed, include:
Link: <https://hub.example/websub>; rel="hub"
Link: <https://feed.example/rss>; rel="self"
The header is authoritative and works for any content type, so it's the preferred mechanism.
<atom:link> in the feed (backup). Inside the feed document, declare the Atom
namespace and add the hub and self links — useful for consumers that only read the
body:
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<atom:link rel="hub" href="https://hub.example/websub"/>
<atom:link rel="self" href="https://feed.example/rss"/>
<!-- … -->
</channel>
</rss>
A subscriber reads the rel="hub" link to find this endpoint and the rel="self"
link to learn the canonical topic URL, then subscribes as above. (Note: discovery is a
subscriber-side concern — this hub accepts explicit hub.topic subscriptions
regardless of how the feed advertises itself.)