Skip to content

Conversation

emersion
Copy link
Contributor

@emersion emersion commented Sep 17, 2021

A primary goal of this spec is to not tie the extension to any proprietary push service. This is achieved by using the standard Web Push protocol. Web Push also provides encryption of the payload.

IRC clients are responsible for providing a push endpoint to the IRC server. This is usually provided by the platform (e.g. by the browser for the Web). IRC servers don't need to register or anything like that, all they need is send an HTTP request when they wish to deliver a push notification.

See the spec for more details.

Not mentioned in the spec is the integration with proprietary push services. For instance, Android apps can't (unfortunately) use Web Push: they must use Firebase Cloud Messaging (FCM). Since FCM is also used on Chrome for their Web Push implementation, I've tried to see if there was a way to figure out some Web Push parameters from the FCM ones, but it sounds like the Web Push functionality is limited to the Chrome API keys.

My approach is to introduce a relay which listens for Web Push messages and forwards them to FCM. This does require an additional moving part that IRC clients need to rely on, however this sounds like a reasonable trade-off to me. Since the relay doesn't see any clear-text privacy-sensitive information (payload is encrypted by the IRC server, no client IP address is visible), I could provide a free relay service for client developers who don't want to setup their own.

      ┌────────────┐              ┌────────────┐
      │            │  Subscribe   │            │
      │   Android  ├─────────────►│            │
      │ IRC client │              │ IRC server │
      │            │              │            │
      │            │              │            │
      └────────────┘              └─────┬──────┘
             ▲                          │
             │                          │
        Push │                          │Push
notification │                          │notification
             │                          ▼
       ┌─────┴─────┐              ┌──────────┐
       │           │              │          │
       │ Firebase  │◄─────────────┤ Web Push │
       │ Messaging │              │   Relay  │
       │           │              │          │
       └───────────┘              └──────────┘

So far, I've implemented proof-of-concepts for soju and two client platforms:

  • gamja (Web): the implementation is pretty straightforward, it's mostly a matter of piping the JS API to IRC.
  • Android: this requires a relay, and requires to implement Web Push decryption logic in the client. This isn't too bad, the Web Push encryption and decryption logic is mostly symmetrical. So as long as a library to send Web Push notifications exists, it shouldn't be too hard to turn it the other way around and make it decrypt notifications.

References:


This is an incomplete specification. I opened this PR to gather feedback on the approach. Let me know what you think!

  • WEBPUSH UNREGISTER
  • Figure out how to specify "messages of interest"
  • Expand on TTLs
  • Expand on rate limiting
  • Allow servers to drop message tags to fit the notification payload size limit
  • Consider turning the cap into an ISUPPORT
  • Consider sending the VAPID public key as an ISUPPORT
  • Error codes

@hhirtz
Copy link

hhirtz commented Sep 17, 2021

Sorry if this is obvious, but if the server needs to wait for the client to send the endpoint, what is the use of the webpush capability? Also it's not present in soju and gamja's commits.

@emersion
Copy link
Contributor Author

It's used to signal the client that the commands are supported. Could use an ISUPPORT I suppose… but need to make sure servers don't want to change behavior when push notifications are enabled.

@DarthGandalf
Copy link
Member

capability doesn't prevent server from changing the behavior after cap was enabled. There's even CAP DEL especially for that

@emersion
Copy link
Contributor Author

capability doesn't prevent server from changing the behavior after cap was enabled. There's even CAP DEL especially for that

Nah, I mean that a cap would inform the server that the client supports the ext. For instance the chat history spec uses this to make servers not send playback on connect when the chathistory cap is enabled.

@DarthGandalf
Copy link
Member

Okay, and why this information is needed upon connect? I understand why it's needed for chathistory

@emersion
Copy link
Contributor Author

As said above, I'm not necessarily opposed to making this an ISUPPORT instead of a cap. We just need to make sure that no server is interested in knowing whether the client supports the ext or not.

## Implementation

The `webpush` capability allows clients to subscribe to Web Push and receive
notifications for messages of interest.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does a user/client indicate which messages would be of interest? There doesn't appear to be mechanisms in-place in the currently offered commands to indicate which messages would be of interest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, as discussed in #ircv3, this part isn't addressed yet. For now, it's spec'ed as a "server-defined subset of IRC messages".

I think there are multiple ways to go about this:

  1. Keep as-is, let servers decide completely. Leave it to a future extension to let clients indicate messages of interest. This can be useful for chathistory as well, to avoid fetching all unread history when connecting.
  2. Indicate a minimal set of messages to forward (e.g. PRIVMSG/NOTICE/INVITE with user as target, plus PRIVMSG/NOTICE highlights). Let servers send more if they want to. Clients can always ignore some push notifications.
  3. Build a webpush-specific command to indicate the messages of interest. This is a slippery slope, because the rules can be pretty complicated. If we go down this road I'd rather keep it simple and have just simple word matching.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (1) is the superior option here.

@slingamn
Copy link
Contributor

Two issues that came up in discussion with @kylef:

  1. Signal only sends a ping message (i.e. with no user data) over the push channel, and then the client reconnects to the server and downloads the actual message data. This was felt to be unworkable in the IRC context, since iOS is imposing increasingly onerous restrictions on what can be done in a push notification handler (it's too hard to connect to an IRC server, and soon it may be outright impossible).
  2. However, RFC8030 seems to impose a de facto size limit of 4096 bytes on push messages. This is shorter than the maximum message length under message-tags, so provisions for the server shortening the message are necessary. (The server might have a list of tags to prioritize?) Given this, we might want to specify an alternative format for the message (JSON would allow the server to send extensible metadata with the push message, although at this time I don't have a candidate use case).

@emersion
Copy link
Contributor Author

Yeah, the size limit is going to be an issue. FCM alone also has a 4096 bytes limit for the payload.

we might want to specify an alternative format for the message

I don't think JSON would help, the limit still applies regardless of the format of the payload. Also, servers can extend messages already with tags. Am I missing something?

@slingamn
Copy link
Contributor

Sorry, that was unclear. I meant something like: abandon RFC1459-and-derivatives entirely in the format of the push message, parse prefix/tags/command/params directly into JSON.

@emersion
Copy link
Contributor Author

How is this going to help with the size limit of the push notification payload?


The `<endpoint>` is an URL pointing to a push server, which can be used to send push messages for this particular subscription.

`<keys>` is a string encoded in the message-tag format. The values are [URL-safe base64-encoded][RFC 4648 section 5]. It MUST contain at least:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses the URL-safe base64 encoding because that's what the W3C API uses. So this allows Web clients to just use the result of subscription.toJSON().keys as-is.

But maybe that isn't such a good idea. The W3C API also doesn't provide any function to encode an arbitrary byte buffer to URL-safe base64, so users need to roll out their own if we use it elsewhere.

Instead we could just use regular base64, just like SASL does. Users can call subscription.getKey().

@kylef
Copy link
Contributor

kylef commented Sep 19, 2021

Do you intent to add a command to view the list of subscriptions? I could see how as a user this could be useful to debug problems and be able to manually clean up or unsubscribe old clients for which you do not know the subscription URL for. This could very well be something that a NickServ or similar service could facilitate for a user and not be included in the spec though.

@emersion
Copy link
Contributor Author

Yeah, if it's just a debugging tool, I think I'd rather let servers include it as a special service or custom commands. Also need to investigate if/how subscription expiration can be integrated.

@progval
Copy link
Contributor

progval commented Oct 6, 2021

@emersion
Copy link
Contributor Author

emersion commented Oct 6, 2021

UnifiedPush is basically Web Push without any kind of encryption. I've been discussing with them to add encryption, they seem open to the idea (they considered Web Push in the past but didn't understand fully how encryption works).

emersion added a commit to emersion/soju that referenced this pull request Nov 27, 2021
emersion added a commit to emersion/soju that referenced this pull request Mar 6, 2022
emersion added a commit to emersion/soju that referenced this pull request Mar 6, 2022
emersion added a commit to emersion/soju that referenced this pull request Mar 7, 2022
emersion added a commit to emersion/soju that referenced this pull request Mar 10, 2022
@emersion
Copy link
Contributor Author

My plan is to go ahead and ship this as a soju vendored extension, then gather field testing feedback and report back what I've learnt here.

emersion added a commit to emersion/soju that referenced this pull request Mar 11, 2022
@emersion
Copy link
Contributor Author

The vendored extension has been merged in soju and goguma.

@emersion emersion marked this pull request as ready for review September 28, 2022 15:59
@emersion
Copy link
Contributor Author

Goguma and pushgarden now support Apple Push Notification service, as more evidence that this approach works on all platforms.

@emersion
Copy link
Contributor Author

emersion commented Feb 3, 2025

The vendored extension is now supported by Igloo and Ergo.

Copy link
Contributor

@slingamn slingamn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me say, thanks very much for your work on this spec; it's well-designed and solves an important problem.

More discussion of implementation considerations and security considerations would be helpful. For example, the implications of sending a POST to a user-chosen address should be discussed in more detail. The Soju restrictions (which I copied in Ergo) are to only allow https endpoints, and to disallow contacting loopback or internal IP addresses.


If the server supports [Voluntary Application Server Identification (VAPID)][RFC 8292] and the client has enabled the `draft/webpush` capability, the server MUST advertise its public key in the `VAPID` ISUPPORT token. This key can be used to verify notifications upon reception by the Web Push server.

The value MUST be the [URL-safe base64-encoded][RFC 4648 section 5] public key usable with the Elliptic Curve Digital Signature Algorithm (ECDSA) over the P-256 curve. The value MUST NOT change over the lifetime of the connection to avoid race conditions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the connection referenced here? Is it the transient IRC connection established by the client in order to send WEBPUSH REGISTER?

It's not clear on the basis of this spec how to do key rotation, or if it's even possible (I'm not sure what the right answer is, but the MUST NOT here seems excessive).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the IRC TCP connection.

The spec has been designed to allow key rotation. The server can advertise a different key to new connections. However, a server needs to keep the old key working or risk breaking previous subscriptions. For instance, maybe a client hasn't connected via TCP for a month but still receives push notifications every week or so.

The race condition is the following:

  • Client connects and receives VAPID 1. Client sends a WEBPUSH REGISTER command.
  • Server sends a new VAPID 2.
  • Server receives WEBPUSH REGISTER, but can't tell whether it must use VAPID 1 or VAPID 2 when sending push notifications.

It's important to use the correct VAPID, because the push endpoint should validate the VAPID.

We could add ways to fix the race (e.g. by adding some kind of ACK mechanism, simplest would be for the client to send the VAPID in WEBPUSH REGISTER perhaps), but I figured it wasn't worth it since a server needs to keep the old key working anyways and the typical TCP connection lifetime is very small compared to the typical push registration lifetime.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, that makes sense. Here's the disconnect I was having:

the typical TCP connection lifetime is very small compared to the typical push registration lifetime

This is not true of traditional or hybrid IRC setups where connections can live for weeks. The current spec language seems to imply that the VAPID key cannot be rotated as long as there is any open TCP connection (that hasn't already sent WEBPUSH REGISTER?). This would make rotation impossible in practice.

It seems to me that the race window from rotation is very small as long as the server notifies active clients about the rotation, by sending them a new 005 line with the new value.

What is the expected behavior if the push endpoint fails to validate the VAPID signature? 404?

Copy link
Contributor Author

@emersion emersion May 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true of traditional or hybrid IRC setups where connections can live for weeks

Web Push is useful in settings where the TCP connection is short-lived.

The current spec language seems to imply that the VAPID key cannot be rotated as long as there is any open TCP connection (that hasn't already sent WEBPUSH REGISTER?). This would make rotation impossible in practice.

The way it's designed is that the server would keep old keys. When a new TCP connection is established, the new keys are sent. When a Web Push registration is renewed, the new keys can be used. While an old Web Push subscription is active, the server cannot throw away the old keys.

A server cannot assume that a client will use the new VAPID keys as soon as the server advertises them:

  • The client might be disconnected while the key rotation is performed. The client needs to tell the Web Push server about the new VAPID keys. The client might not reconnect for a while (multiple days).
  • Web Push is already quite tricky to implement without dynamic key rotation. I haven't implemented dynamic key rotation in my clients because it would add more complexity for little benefit. I assume many other client authors would feel the same.

What is the expected behavior if the push endpoint fails to validate the VAPID signature? 404?

RFC 8292 section 4.2 says it should be a 403.


### Errors

Errors are returned using the standard replies syntax.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a FAIL WEBPUSH FORBIDDEN code to address situations where the command is administratively disallowed for any reason (e.g. too many existing subscriptions, or the user is not logged into an account).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably the server could use cap-notify to add/remove the cap dynamically depending on user auth status? That would better reflect the server policy: clients don't display an error when unauthenticated, and clients can enable web push on CAP NEW.

It's true that right now we only have INTERNAL_ERROR which might not be appropriate for cases such as limits (per-user limit, per-endpoint-host limit, rate limit, etc).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we advertise the CAP to unauthenticated users pre-registration, and then remove it after the 001? That seems awkward.

And yeah, FORBIDDEN seems justified for other cases like limits.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the server would hide the cap pre-conn-reg, and send CAP NEW after authentication.

A FORBIDDEN error will be bubbled up to the user, which is unlikely to be what you want here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a MAX_REGISTRATIONS code for this case.


The `draft/webpush` capability allows clients to subscribe to Web Push and receive notifications for messages of interest.

Once a client has subscribed, the server will send push notifications for a server-defined subset of IRC messages. Each push notification MUST contain exactly one IRC message as the payload, without the final CRLF.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that this disallows sending a draft/multiline?

I'm not sure what the right approach is (the practical upper limit on push message sizes appears to be 4096 bytes, and multilines could exceed that), but it's worth thinking about. In my implementation, you'll get the first line of the multiline in the push message (even if it doesn't actually contain the highlight), and it will be tagged with the overall msgid of the multiline (which is the normal fallback behavior).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't considered draft/multiline at all when writing the spec.

I think sending the first line is fair: UX-wise notifications are not designed to display big blobs of text, and the user can always tap to see the whole thing.

@emersion
Copy link
Contributor Author

More discussion of implementation considerations and security considerations would be helpful. For example, the implications of sending a POST to a user-chosen address should be discussed in more detail. The Soju restrictions (which I copied in Ergo) are to only allow https endpoints, and to disallow contacting loopback or internal IP addresses.

Yes, agreed! Also some recommendations around expiration on the server side, how a client should renew registrations, etc.

@p1gp1g
Copy link

p1gp1g commented Feb 21, 2025

I've submitted a draft RFC for an IMAP WebPush extension. This is, as expected, very close to this extension for IRC: https://datatracker.ietf.org/doc/draft-gougeon-imap-webpush/

I have notably added another command to silent a registration, this can be useful when the client sync for some time after a push message and if the client define a "do not disturb" period. This may interest you to add it as well.

Since you are already using your IRC extension, I wonder if there are things I forgot to take in considerations ?

Not mentioned in the spec is the integration with proprietary push services. For instance, Android apps can't (unfortunately) use Web Push: they must use Firebase Cloud Messaging (FCM). Since FCM is also used on Chrome for their Web Push implementation, I've tried to see if there was a way to figure out some Web Push parameters from the FCM ones, but it sounds like the Web Push functionality is limited to the Chrome API keys.

This is actually possible to send Web Push message to FCM servers directly, without a gateway, I've described it here: https://unifiedpush.org/news/20250131_push_for_decentralized/

@slingamn
Copy link
Contributor

Is anyone aware of a proposed post-quantum replacement for RFC8291?

@emersion
Copy link
Contributor Author

emersion commented May 4, 2025

I have notably added another command to silent a registration, this can be useful when the client sync for some time after a push message and if the client define a "do not disturb" period. This may interest you to add it as well.

What is the difference with unregistering (and potentially re-registering later)?

This is actually possible to send Web Push message to FCM servers directly, without a gateway, I've described it here

Great to see you've found a way to do this? A few years back, I've tried reading the Chromium source code and send Web Push payloads directly to FCM servers, but it seemed like that endpoint was restricted to Chrome API keys.

@emersion
Copy link
Contributor Author

emersion commented May 4, 2025

Added sections for security and implementation considerations.

@p1gp1g
Copy link

p1gp1g commented May 5, 2025

I have notably added another command to silent a registration, this can be useful when the client sync for some time after a push message and if the client define a "do not disturb" period. This may interest you to add it as well.

What is the difference with unregistering (and potentially re-registering later)?

The most advantageous is SILWEBPUSH session, when it is used, the client don't have to implement anything when it closes its connection, and it can let itself be killed without worrying to restore the subscription

When SILWEBPUSH duration is used, it doesn't have to implement a scheduled event to restore the subscription; it avoid a connection to send a new REGISTER

@emersion
Copy link
Contributor Author

emersion commented May 5, 2025

I'm not entirely sure I undertsand the use-case here. I don't recommend switching notifications on and off because this can easily result in race conditions and missed notifications. I recommend either always relying on push notifications and not showing any notification from the TCP connection (preferred), or ignoring push messages when the TCP connection is opened (but this is racy).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants