This document captures requirements to evolve protocol handlers as available on the web. The goal is to make it possible to extend the set of protocols that are supported while maintaining a high level of security and trust for users.
Protocol handlers today are focused on single-shot handoff. When activating a mailto:
link,
the browser will either hand it off to the OS for a native application to handle, or will pass it to a
(top-level) URL that will receive it as a parameter and process it. In either case, the interaction
is not incorporated into the current context and no further interaction with the protocol is possible.
In effect, this is more a scheme dispatcher than a comprehensive handler for a given protocol.
Today's on-ramp for new protocols to become used on the web beyond a simple handoff however is too steep. While there is interest from implementers in loosening handler registration, that does not extend to the ability to fully integrate new protocols into the web stack such that they can be the source of top-level documents, iframes, fetch, images, etc.
This document captures requirements for browser extensions to be able to register as full-fledged (or at least fuller-fledged) programmatic protocol handlers. For instance, using this approach an extension could implement support for IPFS directly within any browser context.
One goal is that registering a protocol handler in different ways and at different levels should map to a unified underlying model to the extent possible, so as to ensure that a single code path controls protocol handling.
With progress towards a IANA+blocklist approach as advocated in whatwg/html#9158,
there is no need to limit registration to web+
- or ext+
-prefixed schemes.
The proposed integration method for new protocol handling is to operate a Service Worker for that
protocol that can respond to FetchEvent
s. The assumption is that the browser will
address any and all schemes with HTTP semantics, and it is up to the handler to figure out the
appropriate mapping, knowing that there will likely be impedance mismatch.
The current proposal only exposes this capability to extensions. In that context, registering a
Service Worker for handling by an extension is done using the
protocol_handlers
field. We suggest adding a serviceWorker
field that points to a JS resource in the
extension implementing a Service Worker. The serviceWorker
field takes precedence
over the uriTemplate
field (but having both is not an error, this allows for an
upgrade path). (See notes from lidel
tracking the idea.)
Note that custom protocol handlers of this kind need to be integrated at the same architectural position as
the browser's built-in protocol handling. Notably, they kick in after extension points to block or
modify network requests have been processed. If, for instance, an ad blocker has a denylist that includes
ipfs:
URLs, those will be processed before the IPFS protocol handler is invoked.
In the case of handoff protocol handlers, user agents can simply prompt the user to pick which handler they wish to use after activating a link with a custom protocol. With programmatic handlers that can be called upon to load a subresource, that is not a desirable behavior. In cases where a new handler is being registered for a scheme that there is already a handler for, the user agent is encouraged to pick which one will be used. User agents may simply use the latest handler to be registered, or may prompt the user to select the one they wish to use by default. User agents should avoid having both programmatic handlers and handoff handlers registered for the same scheme as that can create a confusing experience if clicking on a custom protocol link offers to choose between those two but loading a subresource only selects the programmatic one. In such cases, systematically using the the programmatic handler is likely to be the most user-friendly option.
A manifest with a Service Worker handling the protocol looks like this:
{ "manifest_version": 2, "name": "InterPlanetary Wonders", "icons": { "48": "planets.png", "96": "planets@2x.png" }, "version": "17.0.1", "protocol_handlers": [ { "protocol": "ipfs", "name": "IPFS Makes Your Browser Fresh", "serviceWorker": "js/gateway-loader.js" } ] }
And the Service Worker is straightforward:
self.addEventListener("fetch", (ev) => { ev.respondWith( (async () => { const url = new URL(ev.request.url); const gatewayURL = [ 'https://ipfs.io/ipfs/', url.hostname, url.pathname || '/', url.search || '', url.hash || '', ].join(''); const { method, headers, body, mode, credentials, cache, redirect, referrer, referrerPolicy, integrity, signal } = ev.request; return fetch(new Request( gatewayURL, { method, headers, body, mode, credentials, cache, redirect, referrer, referrerPolicy, integrity, signal } )); })(), ); });
In addition to extensions, this level of protocol handling could also be made available to PWAs. In terms of
design and code paths, the same protocol_handlers
entry could be used in the
web app manifests as
we are recommending for extensions. Should that be supported, we don't anticipate the design of protocol
handlers to need to be different.
Extensions are the safest way to support this extension point, and it seems wise to limit support to that pathway at least at first. However, it is good to note that the support could be extended to PWAs without rearchitecting the system and without any change in syntax.
In order to plug a protocol as an extension to the browser's capabilities, we need to address more than simply handing a URL off to an API that returns a stream. While it is possible to expose the many options involved through an API, it is easy enough to shoot yourself in the foot and has enough potential security issues that our preference goes to selecting a strict default behavior instead.
URLs integrate deeply into the web stack and specific schemes can have a host (pun intended) of different behaviors.
In order to integrate into the stack, a new scheme needs to map its URL-space onto origins. However,
not all schemes have host
and port
components.
Proposal. URLs are parsed as per [[url]]. Because the browser cannot guess the default port for that scheme, the API is expected to provide it, if it is meaningful.
URLs may have credentials components, and the basic URL parser will parse them if they are present. These however can be problematic, what with embedding credentials in plain text and all.
Proposal. The browser parses them but removes them from the URL before calling the Service Worker.
In HTTP URLs, paths get canonicalized (e.g. ..
path segments are removed) but that is not the
case with all schemes.
Proposal. Developer expectations are that these canonicalized are resolved and many resources simply assume it. We should canonicalize automatically.
Many protocols aren't just read-only but also allow for writing. The browser needs to know what to do
to handle [[fetch]] verbs beyond GET
and HEAD
as well as form
submissions.
Proposal. One simpler option is to start with read-only; anything else produces a 405
error. A writable iteration could involve using FetchEvent
to process arbitrary HTTP requests
and gateway them to the underlying protocol. Alternatively, we may go straight to the latter if there is
no specific concern with the approach.
We need to determine if schemes are secure (support more powerful APIs, are allowed as mixed content).
Proposal. Extension-supported schemes are considered secure. Extensions are expected to make sure that they do not map to insecure protocols.
Is the scheme subject to CORS gating?
Proposal. Yes. It is easy enough for an extension to handle CORS handshakes itself according to its own preference.
Does this scheme get sent as a referrer for loaded resources or navigations?
Proposal. No. The strict CSP should prevent the loading of resources from content loaded in an extension-supported scheme, which would make a referrer moot. For navigation, the potential tracking risk is not worth the value.
Is this scheme only for resources loaded from another context or can it be loaded at the top level?
Proposal. It can be loaded at the top level.
Does the scheme support setting cookies, using local storage, etc. and if so according to which rules.
Proposal. Extension-supported schemes support local storage and cookies. Implementations are expected to apply partitioning based on the origin (as described above) as they would with an HTTP origin.
We need to integrate into the CSP model for security to work properly.
Proposal. Default to a very strict CSP and don't allow the extension to override it. We can loosen things later if need be.
Two relevant aspects apply with respect to sandboxing:
sandbox
attribute apply to frames that use such schemes?allow-top-navigation-to-custom-protocols
?
Proposal. The sandbox
attribute applies as it does in other contexts, ther is nothing
special about content from custom protocols given that they can be mapped from fetch. These scheme do count
as custom scheme even if they are implemented closer to the metal simply because that list is fixed to all
non-fetch schemes.
More questions need to be answered (some from Eric Lawrence's list):
We also need to look more closely at the details of what browsers do. For instance, Gecko requires that protocols implement
nsIProtocolHandler
, which
returns a nsIChannel
. That doesn't
cover all the integration, though, for instance CSP would happen elsewhere.
Many thanks to the following people, in alphabetical order, for their invaluable input: Brian Kardell, Dave Justice, Dietrich Ayala, Fabrice Desré, Javier Fernández, and lidel.