Specification notes#

This library implements two specifications that mostly agree:

  • WebSub, a W3C Recommendation, the default
  • PubSubHubbub 0.4, its predecessor, behind WithMode(websub.ModePuSH04)

The public API is identical either way. Every difference is a field in the table in mode.go and is looked up rather than branched on at a call site, so the deltas stay auditable against the spec text in one place.

Where they differ#

Behavior WebSub PubSubHubbub 0.4
Lease duration Required. Hubs must enforce expiration and must not issue perpetual leases Optional. An omitted lease means the subscription never expires
Discovery Link header, then HTML <link>, then Atom, then RSS, as a required ordered chain Link header primarily, the rest best effort
Verification Asynchronous GET challenge, always Same mechanism, but synchronous verification is tolerated in some deployments

Two of those have visible consequences:

  • A hub in ModeWebSub refuses hub.lease_seconds=0 with a 400. The same request in ModePuSH04 is accepted as a permanent subscription.
  • Discovery in ModeWebSub sniffs a document whose media type is missing or unrecognized, because the ordered chain is required. ModePuSH04 only parses what the response declares.

Where the specs are silent#

Places the text does not settle, and what this library does.

Publisher to hub notification#

The specific mechanism for the publisher to inform the hub is left unspecified. For example, some existing public hubs ask publishers to send a POST request with the keys hub.mode="publish" and hub.url=(the URL of the resource that was updated).

Publisher.Publish sends exactly that, plus hub.topic with the same value, because a number of hubs read that name instead. Sending both costs one form field.

For content posted directly rather than pinged, the topic travels in a rel=self Link header, which is how a hub identifies content everywhere else in the protocol.

Response to invalid signed content#

The spec says a subscriber must ignore content whose signature does not match, and separately that a callback must return 2xx to indicate success. It does not say what to return for content you are ignoring.

This library returns 2xx and discards. See design decisions for why.

Which hub to use when several are advertised#

If more than one hub URL is specified, it is expected that the publisher notifies each hub, so the subscriber may subscribe to one or more of them.

Discovery.Hubs returns all of them in the order found, and Discovery.Hub() returns the first. Subscriber.Subscribe uses the first. Subscribing at several for redundancy is a caller decision, since it means handling duplicate deliveries.

Publisher.Publish notifies every configured hub, and one failing does not stop the others.

Requirements worth calling out#

These are easy to miss and are each covered by a conformance case.

  • hub.topic must be the self URL found during discovery, which is frequently not the URL discovery started from. A subscriber that echoes back the URL it fetched will silently subscribe to the wrong topic on any site that redirects or canonicalizes.
  • The verification challenge must be echoed with a safe media type. It is attacker influenced text reflected into a response, so a callback that serves it as HTML is a reflected XSS. This library sends text/plain; charset=utf-8 and X-Content-Type-Options: nosniff.
  • A subscriber must answer 404 to a verification request for a topic, mode, or callback it does not recognize. Answering 2xx confirms a subscription it never asked for.
  • A hub must let subscribers re-subscribe to an active subscription, and each request overrides the previous state rather than duplicating or failing.
  • Content distribution must carry both rel=hub and rel=self Link headers, and the content type of the topic rather than one the hub chose.
  • The secret is capped at 200 bytes and should only be sent over an HTTPS callback. This library enforces the cap and logs a warning for the callback, since the spec says should rather than must.

Signature algorithms#

sha1, sha256, sha384, and sha512 are all accepted on verification. Hubs sign with sha256 unless told otherwise. sha1 exists for older subscribers and is never chosen by default.

The header format is method=signature, with the signature in lowercase hex. Verification is constant time, and the parsing is fuzzed, because a forged X-Hub-Signature accepted through a parsing bug is the worst failure this library can have.