Created
April 19, 2019 16:20
-
-
Save Gozala/857e8eecef43aa7407300f35856a88f9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/toolkit/components/extensions/ExtensionProtocol.jsm b/toolkit/components/extensions/ExtensionProtocol.jsm | |
new file mode 100644 | |
index 000000000000..723bc3c77f6c | |
--- /dev/null | |
+++ b/toolkit/components/extensions/ExtensionProtocol.jsm | |
@@ -0,0 +1,964 @@ | |
+/* This Source Code Form is subject to the terms of the Mozilla Public | |
+ * License, v. 2.0. If a copy of the MPL was not distributed with this | |
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | |
+"use strict" | |
+ | |
+const EXPORTED_SYMBOLS = ["ExtensionProtocol"] | |
+const { | |
+ classes: Cc, | |
+ interfaces: Ci, | |
+ utils: Cu, | |
+ results: Cr, | |
+ manager: Cm | |
+} = Components | |
+ChromeUtils.import("resource://gre/modules/Services.jsm") | |
+const { ppmm, cpmm, mm, appinfo } = Services | |
+ | |
+ChromeUtils.defineModuleGetter( | |
+ this, | |
+ "Timer", | |
+ "resource://gre/modules/Timer.jsm" | |
+) | |
+ | |
+ChromeUtils.defineLazyServiceGetter( | |
+ this, | |
+ "UUIDGenerator", | |
+ "@mozilla.org/uuid-generator;1", | |
+ "nsIUUIDGenerator" | |
+) | |
+ | |
+ChromeUtils.defineLazyServiceGetter( | |
+ this, | |
+ "contentSecManager", | |
+ "@mozilla.org/contentsecuritymanager;1", | |
+ "nsIContentSecurityManager" | |
+) | |
+ | |
+ChromeUtils.defineLazyGetter(this, "componentRegistrar", () => | |
+ Components.manager.QueryInterface(Ci.nsIComponentRegistrar) | |
+) | |
+ | |
+const { setTimeout } = Cu.import("resource://gre/modules/Timer.jsm", {}) | |
+const { console } = Cu.import("resource://gre/modules/Console.jsm", {}) | |
+ | |
+const PR_UINT32_MAX = 0xffffffff | |
+ | |
+const isParent = appinfo.processType === appinfo.PROCESS_TYPE_DEFAULT | |
+const { ID } = Components | |
+ | |
+const componentRegistrar = Cm.QueryInterface(Ci.nsIComponentRegistrar) | |
+const pid = `@${appinfo.processType}#${appinfo.processID}` | |
+ | |
+const getFactoryByCID = cid => Cm.getClassObject(cid, Ci.nsIFactory) | |
+ | |
+const getCIDByContractID = contractID => | |
+ componentRegistrar.contractIDToCID(contractID) | |
+ | |
+const getContractIDByScheme = scheme => | |
+ `@mozilla.org/network/protocol;1?name=${scheme}` | |
+ | |
+const getCIDByScheme = scheme => | |
+ getCIDByContractID(getContractIDByScheme(scheme)) || null | |
+ | |
+const getFactoryByProtocolScheme = scheme => { | |
+ const cid = getCIDByScheme(scheme) | |
+ return cid == null ? null : getFactoryByCID(cid) | |
+} | |
+ | |
+const unregisterProtocol = scheme => { | |
+ const cid = getCIDByScheme(scheme) | |
+ const factory = cid && getFactoryByCID(cid) | |
+ if (cid && factory) { | |
+ componentRegistrar.unregisterFactory(cid, factory) | |
+ } | |
+} | |
+ | |
+const isContractIDRegistered = contractID => | |
+ componentRegistrar.isContractIDRegistered(contractID) | |
+ | |
+const isRegisteredProtocol = scheme => | |
+ isContractIDRegistered(getContractIDByScheme(scheme)) | |
+ | |
+const registerProtocol = ({ scheme, uuid }, handler) => { | |
+ const contractID = getContractIDByScheme(scheme) | |
+ if (isContractIDRegistered(contractID)) { | |
+ unregisterProtocol(scheme) | |
+ } | |
+ | |
+ const cid = new ID(uuid) | |
+ const description = `${scheme} protocol handler` | |
+ const factory = new Factory(new ProtocolHandler(scheme, handler)) | |
+ componentRegistrar.registerFactory(cid, description, contractID, factory) | |
+ debug && | |
+ console.log( | |
+ `registerFactory${pid}`, | |
+ cid.toString(), | |
+ contractID, | |
+ factory.instance.scheme, | |
+ isContractIDRegistered(contractID) | |
+ ) | |
+} | |
+ | |
+const LOAD_NORMAL = 0 | |
+ | |
+const IDLE = 0 | |
+const ACTIVE = 1 | |
+const PAUSED = 2 | |
+const CANCELED = 3 | |
+const CLOSED = 4 | |
+const FAILED = 5 | |
+ | |
+const abort = {} | |
+ | |
+const wait = (ms = 0) => new Promise(resolve => Timer.setTimeout(resolve, ms)) | |
+ | |
+const createDict = /*::<a>*/ () /*: { [string]: a } */ => { | |
+ const dict /*: Object */ = Object.create(null) | |
+ return dict | |
+} | |
+ | |
+/*:: | |
+type ReadyState = | |
+ | typeof IDLE | |
+ | typeof ACTIVE | |
+ | typeof PAUSED | |
+ | typeof CANCELED | |
+ | typeof CLOSED | |
+ | typeof FAILED | |
+ | |
+export type RequestStatus = | |
+ | typeof Cr.NS_OK | |
+ | typeof Cr.NS_BASE_STREAM_WOULD_BLOCK | |
+ | typeof Cr.NS_BINDING_ABORTED | |
+*/ | |
+ | |
+class TransportSecurityInfo /*::implements nsITransportSecurityInfo*/ { | |
+ /*:: | |
+ securityState:nsWebProgressState | |
+ shortSecurityDescription:string | |
+ errorCode:nsresult | |
+ errorMessage:string | |
+ SSLStatus:* | |
+ state:string | |
+ */ | |
+ constructor() { | |
+ this.state = "secure" | |
+ this.securityState = Ci.nsIWebProgressListener.STATE_IS_SECURE | |
+ this.errorCode = Cr.NS_OK | |
+ this.shortSecurityDescription = "Content Addressed" | |
+ this.SSLStatus = { | |
+ cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", | |
+ // TLS_VERSION_1_2 | |
+ protocolVersion: 3, | |
+ isDomainMismatch: false, | |
+ isNotValidAtThisTime: true, | |
+ serverCert: { | |
+ subjectName: "Content Addressing", | |
+ displayName: "Content Addressing", | |
+ certType: Ci.nsIX509Cert.CA_CERT, | |
+ isSelfSigned: true, | |
+ validity: {} | |
+ } | |
+ } | |
+ } | |
+ QueryInterface(iid) { | |
+ const isSupported = | |
+ false || | |
+ iid.equals(Ci.nsISupports) || | |
+ iid.equals(Ci.nsITransportSecurityInfo) || | |
+ iid.equals(Ci.nsISSLStatusProvider) | |
+ if (isSupported) { | |
+ return this | |
+ } else { | |
+ throw Cr.NS_ERROR_NO_INTERFACE | |
+ } | |
+ } | |
+} | |
+ | |
+const MAX_UNKNOWN = 0xffffffffffffffff | |
+const UNKNOWN_CONTENT_TYPE = "application/x-unknown-content-type" | |
+ | |
+class Channel /*::implements nsIChannel, nsIRequest*/ { | |
+ /*:: | |
+ URI: nsIURI | |
+ scheme: string | |
+ url: string | |
+ originalURI: nsIURI | |
+ loadInfo: null | nsILoadInfo | |
+ contentCharset: ?string | |
+ contentLength: number | |
+ mimeType: ?string | |
+ byteOffset: number | |
+ requestID: string | |
+ owner: nsISupports<*> | null | |
+ securityInfo: nsITransportSecurityInfo | null | |
+ loadFlags: nsLoadFlags | |
+ loadGroup: nsILoadGroup | |
+ name: string | |
+ status: nsresult | |
+ readyState: ReadyState | |
+ contentDisposition: number | |
+ contentDispositionFilename: string | |
+ contentDispositionHeader: string | |
+ notificationCallbacks: nsIInterfaceRequestor<nsIProgressEventSink> | null; | |
+ | |
+ listener: ?nsIStreamListener | |
+ context: ?nsISupports<mixed> | |
+ handler: RequestHandler | |
+ */ | |
+ constructor( | |
+ uri /*: nsIURI */, | |
+ loadInfo /*: null | nsILoadInfo */, | |
+ requestID /*: string */, | |
+ handler /*: RequestHandler */ | |
+ ) { | |
+ this.URI = uri | |
+ this.url = uri.spec | |
+ this.scheme = uri.scheme | |
+ this.originalURI = uri | |
+ this.loadInfo = loadInfo | |
+ this.originalURI = uri | |
+ this.contentCharset = "utf-8" | |
+ this.contentLength = -1 | |
+ this.mimeType = null | |
+ this.contentDispositionFilename = "" | |
+ this.contentDispositionHeader = "" | |
+ this.byteOffset = 0 | |
+ this.requestID = requestID | |
+ | |
+ this.owner = null | |
+ this.securityInfo = new TransportSecurityInfo() | |
+ this.notificationCallbacks = null | |
+ this.loadFlags = Ci.nsIRequest.LOAD_NORMAL | |
+ this.name = uri.spec | |
+ this.status = Cr.NS_ERROR_NOT_INITIALIZED | |
+ this.readyState = IDLE | |
+ this.handler = handler | |
+ } | |
+ QueryInterface(iid) { | |
+ const isSupported = | |
+ false || | |
+ iid.equals(Ci.nsISupports) || | |
+ iid.equals(Ci.nsIChannel) || | |
+ iid.equals(Ci.nsIRequest) | |
+ if (isSupported) { | |
+ return this | |
+ } else { | |
+ throw Cr.NS_ERROR_NO_INTERFACE | |
+ } | |
+ } | |
+ get contentType() { | |
+ const { mimeType } = this | |
+ if (mimeType != null) { | |
+ return mimeType | |
+ } else { | |
+ return UNKNOWN_CONTENT_TYPE | |
+ } | |
+ } | |
+ set contentType(_) {} | |
+ toJSON() { | |
+ return { | |
+ scheme: this.URI.scheme, | |
+ url: this.URI.spec, | |
+ readyState: this.readyState, | |
+ status: this.status, | |
+ contentType: this.contentType, | |
+ byteOffset: this.byteOffset, | |
+ contentLength: this.contentLength | |
+ } | |
+ } | |
+ open2() { | |
+ // throws an error if security checks fail | |
+ contentSecManager.performSecurityCheck(this, null) | |
+ return this.open() | |
+ } | |
+ open() { | |
+ throw Cr.NS_BASE_STREAM_WOULD_BLOCK | |
+ } | |
+ asyncOpen2(listener) { | |
+ // throws an error if security checks fail | |
+ var outListener = contentSecManager.performSecurityCheck(this, listener) | |
+ return this.asyncOpen(outListener, null) | |
+ } | |
+ asyncOpen(listener, context) { | |
+ // TODO: Make sure that we report status updates | |
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIProgressEventSink | |
+ | |
+ debug && console.log(`asyncOpen${pid} ${JSON.stringify(this)}`) | |
+ switch (this.readyState) { | |
+ case IDLE: { | |
+ this.listener = listener | |
+ this.context = context | |
+ this.status = Cr.NS_OK | |
+ this.loadGroup.addRequest(this, context) | |
+ return this.handler.request(this) | |
+ } | |
+ default: { | |
+ throw this.status | |
+ } | |
+ } | |
+ } | |
+ | |
+ isPending() { | |
+ switch (this.readyState) { | |
+ case ACTIVE: | |
+ case PAUSED: { | |
+ return true | |
+ } | |
+ default: { | |
+ return false | |
+ } | |
+ } | |
+ } | |
+ | |
+ cancel(status = Cr.NS_BINDING_ABORTED) { | |
+ debug && console.log(`cancel(${status})${pid} ${JSON.stringify(this)}`) | |
+ const { handler, readyState } = this | |
+ if (handler) { | |
+ switch (readyState) { | |
+ case ACTIVE: | |
+ case PAUSED: { | |
+ this.setStatus(status) | |
+ return handler.updateRequest(this, Cr.NS_BINDING_ABORTED) | |
+ } | |
+ default: { | |
+ throw this.status | |
+ } | |
+ } | |
+ } | |
+ } | |
+ suspend() { | |
+ debug && console.log(`suspend${pid} ${JSON.stringify(this)}`) | |
+ switch (this.readyState) { | |
+ case ACTIVE: { | |
+ this.readyState = PAUSED | |
+ return this.handler.updateRequest(this, Cr.NS_BASE_STREAM_WOULD_BLOCK) | |
+ } | |
+ case PAUSED: { | |
+ return void this | |
+ } | |
+ default: { | |
+ throw this.status | |
+ } | |
+ } | |
+ } | |
+ resume() { | |
+ debug && console.log(`resume${pid} ${JSON.stringify(this)}`) | |
+ switch (this.readyState) { | |
+ case ACTIVE: { | |
+ return void this | |
+ } | |
+ case PAUSED: { | |
+ this.readyState = ACTIVE | |
+ return this.handler.updateRequest(this, Cr.NS_OK) | |
+ } | |
+ default: { | |
+ throw this.status | |
+ } | |
+ } | |
+ } | |
+ | |
+ setStatus(status) { | |
+ switch (status) { | |
+ case Cr.NS_OK: | |
+ case Cr.NS_BINDING_ABORTED: { | |
+ this.readyState = CANCELED | |
+ this.status = Cr.NS_BINDING_ABORTED | |
+ return this | |
+ } | |
+ default: { | |
+ this.readyState = FAILED | |
+ this.status = status | |
+ return this | |
+ } | |
+ } | |
+ } | |
+ | |
+ head({ contentType, contentLength, contentCharset }) { | |
+ debug && | |
+ console.log( | |
+ `head${pid} ${JSON.stringify({ | |
+ contentType, | |
+ contentLength, | |
+ contentCharset | |
+ })}` | |
+ ) | |
+ | |
+ if (contentType) { | |
+ this.mimeType = contentType | |
+ } | |
+ | |
+ if (contentLength) { | |
+ this.contentLength = contentLength | |
+ } | |
+ | |
+ if (contentCharset) { | |
+ this.contentCharset = contentCharset | |
+ } | |
+ | |
+ this.status = Cr.NS_OK | |
+ this.readyState = ACTIVE | |
+ this.byteOffset = 0 | |
+ | |
+ // If contentType is known start request, otherwise defer until it | |
+ // can be inferred on first data chunk. | |
+ if (this.mimeType != null) { | |
+ const { listener, context } = this | |
+ try { | |
+ listener && listener.onStartRequest(this, context) | |
+ } catch (_) { | |
+ console.error(_) | |
+ } | |
+ } | |
+ } | |
+ body({ content }) { | |
+ const stream = Cc[ | |
+ "@mozilla.org/io/arraybuffer-input-stream;1" | |
+ ].createInstance(Ci.nsIArrayBufferInputStream) | |
+ const { byteLength } = content | |
+ stream.setData(content, 0, byteLength) | |
+ | |
+ const { listener, context } = this | |
+ | |
+ // If mimeType is not set then we need detect it from the arrived content | |
+ // and start request. We know start was deffered so that we would could | |
+ // detect contentType. | |
+ if (this.mimeType == null) { | |
+ try { | |
+ const contentSniffer = Cc[ | |
+ "@mozilla.org/network/content-sniffer;1" | |
+ ].createInstance(Ci.nsIContentSniffer) | |
+ this.mimeType = contentSniffer.getMIMETypeFromContent( | |
+ this, | |
+ new Uint8Array(content), | |
+ byteLength | |
+ ) | |
+ } catch (_) {} | |
+ | |
+ listener && listener.onStartRequest(this, context) | |
+ } | |
+ | |
+ debug && | |
+ console.log( | |
+ `body${pid} ${JSON.stringify( | |
+ this | |
+ )} ${stream.available()} ${byteLength} ${content.toString()} ` | |
+ ) | |
+ | |
+ listener && listener.onDataAvailable(this, context, stream, 0, byteLength) | |
+ this.byteOffset += byteLength | |
+ } | |
+ | |
+ end({ status }) { | |
+ this.readyState = CLOSED | |
+ this.status = status | |
+ this.contentLength = this.byteOffset | |
+ debug && console.log(`end${pid} ${JSON.stringify(this)}`) | |
+ this.close() | |
+ } | |
+ abort() { | |
+ debug && console.log(`abort${pid} ${JSON.stringify(this)}`) | |
+ this.readyState = CANCELED | |
+ this.status = Cr.NS_BINDING_ABORTED | |
+ this.close() | |
+ } | |
+ close() { | |
+ const { listener, context, status } = this | |
+ debug && console.log(`close${pid} ${JSON.stringify(this)}`) | |
+ delete this.listener | |
+ delete this.context | |
+ delete this.handler | |
+ try { | |
+ listener && listener.onStopRequest(this, context, status) | |
+ | |
+ this.loadGroup.removeRequest(this, context, status) | |
+ } catch (_) { | |
+ debug && console.error(`Failed onStopRequest${pid} ${_}`) | |
+ } | |
+ } | |
+} | |
+ | |
+class ProtocolHandler /*::implements nsIProtocolHandler*/ { | |
+ /*:: | |
+ scheme: string | |
+ defaultPort: number | |
+ handler: RequestHandler | |
+ protocolFlags: number | |
+ */ | |
+ constructor(scheme, handler) { | |
+ this.scheme = scheme | |
+ this.defaultPort = -1 | |
+ this.handler = handler | |
+ this.protocolFlags = | |
+ Ci.nsIProtocolHandler.URI_STD | | |
+ Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE | | |
+ Ci.nsIProtocolHandler.URI_IS_POTENTIALLY_TRUSTWORTHY | |
+ } | |
+ toJSON() { | |
+ return { | |
+ scheme: this.scheme, | |
+ defaultPort: this.defaultPort, | |
+ protocolFlags: this.protocolFlags | |
+ } | |
+ } | |
+ allowPort(port, scheme) { | |
+ return false | |
+ } | |
+ newURI(spec, charset, baseURI) { | |
+ debug && | |
+ console.log(`newURI${pid} ${spec} ${String(baseURI && baseURI.spec)}`) | |
+ try { | |
+ const url = Cc["@mozilla.org/network/standard-url-mutator;1"] | |
+ .createInstance(Ci.nsIStandardURLMutator) | |
+ .init( | |
+ Ci.nsIStandardURL.URLTYPE_AUTHORITY, | |
+ this.defaultPort, | |
+ spec, | |
+ charset, | |
+ baseURI | |
+ ) | |
+ .finalize() | |
+ | |
+ return url | |
+ } catch (_) { | |
+ const resolvedSpec = baseURI == null ? spec : baseURI.resolve(spec) | |
+ | |
+ return Cc["@mozilla.org/network/simple-uri-mutator;1"] | |
+ .createInstance(Ci.nsIURIMutator) | |
+ .setSpec(resolvedSpec) | |
+ .finalize() | |
+ } | |
+ } | |
+ newChannel(uri /*: nsIURI */) { | |
+ debug && | |
+ console.log(`newChannel(${uri.spec})${pid} ${JSON.stringify(this)}`) | |
+ return this.newChannel2(uri, null) | |
+ } | |
+ newChannel2(uri /*: nsIURI */, loadInfo /*: nsILoadInfo | null */) { | |
+ debug && | |
+ console.log(`newChannel2(${uri.spec})${pid} ${JSON.stringify(this)}`) | |
+ | |
+ return this.handler.channel(uri, loadInfo) | |
+ } | |
+ QueryInterface(iid) { | |
+ if (iid.equals(Ci.nsIProtocolHandler) || iid.equals(Ci.nsISupports)) { | |
+ return this | |
+ } | |
+ throw Cr.NS_ERROR_NO_INTERFACE | |
+ } | |
+} | |
+ | |
+class Factory /*::implements nsIFactory<nsIProtocolHandler>*/ { | |
+ /*:: | |
+ instance: nsIProtocolHandler | |
+ */ | |
+ constructor(instance /*: nsIProtocolHandler */) { | |
+ this.instance = instance | |
+ } | |
+ createInstance( | |
+ outer /*: null | nsISupports<nsIProtocolHandler> */, | |
+ iid /*: nsIIDRef<nsIProtocolHandler> */ | |
+ ) /*: nsIProtocolHandler */ { | |
+ if (outer != null) { | |
+ throw Cr.NS_ERROR_NO_AGGREGATION | |
+ } | |
+ | |
+ return this.instance | |
+ } | |
+ lockFactory(lock /*: boolean */) /*: void */ { | |
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED | |
+ } | |
+ QueryInterface( | |
+ iid /*: nsIIDRef<nsIFactory<nsIProtocolHandler>> */ | |
+ ) /*: nsIFactory<nsIProtocolHandler> */ { | |
+ if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIFactory)) { | |
+ return this | |
+ } | |
+ console.log(`!!! Factory.QueryInterface ${iid.name} ${iid.number}\n`) | |
+ throw Cr.NS_ERROR_NO_INTERFACE | |
+ } | |
+} | |
+ | |
+const PROTOCOLS = `libdweb:protocol:protocols` | |
+const REGISTER = `libdweb:protocol:register` | |
+const INSTALL = `libdweb:protocol:install` | |
+const REQUEST = `libdweb:protocol:request` | |
+const REQUEST_UPDATE = `libdweb:protocol:request:update` | |
+const RESPONSE_HEAD = `libdweb:protocol:response:head` | |
+const RESPONSE_BODY = `libdweb:protocol:response:body` | |
+const RESPONSE_END = `libdweb:protocol:response:end` | |
+ | |
+const AGENT_INBOX = `libdweb:protocol:agent:inbox` | |
+const AGENT_OUTBOX = `libdweb:protocol:agent:outbox` | |
+const HANDLER_INBOX = `libdweb:protocol:handler:inbox` | |
+const HANDLER_OUTBOX = `libdweb:protocol:handler:outbox` | |
+ | |
+class RequestHandler { | |
+ /*:: | |
+ requestID: number | |
+ +requests: { [string]: Channel } | |
+ +pid: string | |
+ */ | |
+ constructor() { | |
+ this.requestID = 0 | |
+ this.requests = createDict() | |
+ this.pid = `Handler${pid}` | |
+ } | |
+ QueryInterface(iid /*: nsIIDRef<nsIMessageListener<any>> */) { | |
+ if (iid.equals(Ci.nsISupports) || iid.equals(Ci.nsIMessageListener)) { | |
+ return this | |
+ } | |
+ throw Cr.NS_ERROR_NO_INTERFACE | |
+ } | |
+ channel( | |
+ url /*: nsIURI */, | |
+ loadInfo /*: null | nsILoadInfo */ | |
+ ) /*: Channel */ { | |
+ const { scheme } = url | |
+ const requestID = `${scheme}:${++this.requestID}:${this.pid}` | |
+ const request = new Channel(url, loadInfo, requestID, this) | |
+ this.requests[requestID] = request | |
+ return request | |
+ } | |
+ request(channel /*: Channel */) {} | |
+ updateRequest(channel /*: Channel */, status /*: RequestStatus */) {} | |
+} | |
+ | |
+/*:: | |
+export type Register = { | |
+ type: "register", | |
+ scheme: string, | |
+ uuid: string | |
+} | |
+ | |
+export type Unregister = { | |
+ type: "unregister", | |
+ scheme: string, | |
+ uuid: string | |
+} | |
+ | |
+export type Terminate = { | |
+ type: "terminate" | |
+} | |
+ | |
+export type Install = { | |
+ type: "install", | |
+ scheme: string | |
+} | |
+ | |
+export type Head = { | |
+ type: "head", | |
+ requestID: string, | |
+ contentType?: string, | |
+ contentLength?: number, | |
+ contentCharset?: string, | |
+ contentDispositionFilename?: string, | |
+ contentDispositionHeader?:string | |
+} | |
+ | |
+export type Body = { | |
+ type: "body", | |
+ requestID: string, | |
+ content: ArrayBuffer | |
+} | |
+ | |
+export type End = { | |
+ type: "end", | |
+ status:nsresult, | |
+ requestID: string | |
+} | |
+ | |
+export type Response = Head | Body | End | |
+ | |
+export type HandlerOutbox = { | |
+ name: "libdweb:protocol:handler:outbox", | |
+ data: Install | Response, | |
+ target: { messageManager: Out<HandlerInbox> } | |
+} | |
+ | |
+export type HandlerInbox = { | |
+ name: "libdweb:protocol:handler:inbox", | |
+ data: Request | RequestUpdate, | |
+ target: Out<HandlerOutbox> | |
+} | |
+ | |
+export type ProtocolSpec = { scheme: string, uuid: string } | |
+ | |
+type Request = { | |
+ type: "request", | |
+ requestID: string, | |
+ url: string, | |
+ scheme: string, | |
+ status?: void | |
+} | |
+ | |
+type RequestUpdate = { | |
+ type: "requestUpdate", | |
+ requestID: string, | |
+ scheme: string, | |
+ status: RequestStatus | |
+} | |
+ | |
+export type AgentInbox = { | |
+ name: "libdweb:protocol:agent:inbox", | |
+ data: Register | Unregister | Terminate | Response | |
+} | |
+ | |
+export type AgentOutbox = { | |
+ name: "libdweb:protocol:agent:outbox", | |
+ data: Request | RequestUpdate, | |
+ target: nsIMessageSender<AgentInbox> | |
+} | |
+ | |
+export type Inn<a> = nsIMessageListenerManager<a> | |
+export type Out<a> = nsIMessageSender<a> | |
+*/ | |
+class Supervisor extends RequestHandler { | |
+ /*:: | |
+ +protocols: { [string]: ProtocolSpec } | |
+ +handlers: { [string]: Out<HandlerInbox> } | |
+ +agents: { [string]: Out<AgentInbox> } | |
+ +agentsPort: nsIMessageBroadcaster<*, AgentInbox> | |
+ */ | |
+ constructor() { | |
+ super() | |
+ this.protocols = createDict() | |
+ this.handlers = createDict() | |
+ this.agents = createDict() | |
+ | |
+ this.pid = `Supervisor${pid}` | |
+ | |
+ this.agentsPort = ppmm | |
+ } | |
+ receiveMessage(message /*: AgentOutbox | HandlerOutbox */) { | |
+ debug && | |
+ console.log( | |
+ `Receive message:${message.name} at ${this.pid} ${JSON.stringify( | |
+ message.data | |
+ )}`, | |
+ message.target | |
+ ) | |
+ | |
+ switch (message.name) { | |
+ case AGENT_OUTBOX: | |
+ return this.receiveAgentMessage(message) | |
+ case HANDLER_OUTBOX: | |
+ return this.receiveHandlerMessage(message) | |
+ } | |
+ } | |
+ receiveAgentMessage({ data, target } /*:AgentOutbox*/) { | |
+ const { handlers, agents, pid } = this | |
+ const { scheme, requestID } = data | |
+ const handler = handlers[scheme] | |
+ if (handler) { | |
+ debug && | |
+ console.log( | |
+ `-> request${this.pid} ${JSON.stringify(data)}`, | |
+ target, | |
+ handler | |
+ ) | |
+ agents[requestID] = target | |
+ handler.sendAsyncMessage(HANDLER_INBOX, data) | |
+ } | |
+ } | |
+ receiveHandlerMessage({ data, target } /*:HandlerOutbox*/) { | |
+ switch (data.type) { | |
+ case "install": | |
+ return this.register(data.scheme, target.messageManager) | |
+ default: | |
+ return this.forwardResponse(data) | |
+ } | |
+ } | |
+ forwardResponse(response /*:Response*/) { | |
+ debug && console.log(`-> response${this.pid} ${JSON.stringify(response)}`) | |
+ const { agents } = this | |
+ const { requestID } = response | |
+ const agent = agents[requestID] | |
+ if (agent) { | |
+ if (response.type === "end") { | |
+ delete agents[requestID] | |
+ } | |
+ agent.sendAsyncMessage(AGENT_INBOX, response) | |
+ } | |
+ } | |
+ register(scheme /*: string */, handler /*: Out<HandlerInbox> */) { | |
+ const { protocols, handlers } = this | |
+ if (handlers[scheme]) { | |
+ handlers[scheme] = handler | |
+ } else { | |
+ const uuid = UUIDGenerator.generateUUID().toString() | |
+ const protocol = { type: "register", scheme, uuid } | |
+ protocols[scheme] = protocol | |
+ handlers[scheme] = handler | |
+ registerProtocol(protocol, this) | |
+ this.agentsPort.broadcastAsyncMessage(AGENT_INBOX, protocol) | |
+ } | |
+ } | |
+ unregister({ scheme, uuid } /*: ProtocolSpec*/) { | |
+ const { protocols, handlers } = this | |
+ if (protocols[scheme] != null) { | |
+ delete protocols[scheme] | |
+ delete handlers[scheme] | |
+ | |
+ const unregister = { type: "unregister", scheme, uuid } | |
+ unregisterProtocol(scheme) | |
+ this.agentsPort.broadcastAsyncMessage(AGENT_INBOX, unregister) | |
+ } | |
+ } | |
+ terminate() { | |
+ debug && console.log(`Terminate ${this.pid}`) | |
+ const { protocols, requests } = this | |
+ | |
+ this.agentsPort.broadcastAsyncMessage(AGENT_INBOX, { type: "terminate" }) | |
+ ppmm.removeDelayedProcessScript(Components.stack.filename) | |
+ mm.removeMessageListener(HANDLER_OUTBOX, this) | |
+ ppmm.removeMessageListener(AGENT_INBOX, this) | |
+ | |
+ delete this.request | |
+ delete this.protocols | |
+ delete this.agents | |
+ delete this.handlers | |
+ | |
+ for (const scheme in protocols) { | |
+ unregisterProtocol(scheme) | |
+ } | |
+ } | |
+ static new() { | |
+ const self = new this() | |
+ debug && console.log(`new ${self.pid}`) | |
+ ppmm.initialProcessData[PROTOCOLS] = self.protocols | |
+ | |
+ debug && | |
+ console.log(`initialProcessData`, ppmm.initialProcessData[PROTOCOLS]) | |
+ | |
+ ppmm.loadProcessScript(Components.stack.filename, true) | |
+ | |
+ mm.addMessageListener(HANDLER_OUTBOX, self) | |
+ ppmm.addMessageListener(AGENT_OUTBOX, self) | |
+ | |
+ return self | |
+ } | |
+} | |
+ | |
+class Agent extends RequestHandler { | |
+ /*:: | |
+ outbox: Out<AgentOutbox> | |
+ inbox: Inn<AgentInbox> | |
+ protocols: {[string]:ProtocolSpec} | |
+ */ | |
+ constructor() { | |
+ super() | |
+ this.pid = `Agent${pid}` | |
+ this.requestID = 0 | |
+ this.outbox = cpmm | |
+ this.inbox = cpmm | |
+ this.protocols = createDict() | |
+ } | |
+ register(protocol /*: ProtocolSpec */) { | |
+ const { protocols } = this | |
+ | |
+ if (protocols[protocol.scheme] == null) { | |
+ protocols[protocol.scheme] = protocol | |
+ registerProtocol(protocol, this) | |
+ } | |
+ } | |
+ unregister(protocol /*: ProtocolSpec*/) { | |
+ const { protocols } = this | |
+ if (protocols[protocol.scheme] != null) { | |
+ delete protocols[protocol.scheme] | |
+ unregisterProtocol(protocol.scheme) | |
+ } | |
+ } | |
+ request(channel /*: Channel */) { | |
+ const { url, scheme, requestID } = channel | |
+ debug && console.log(`request${this.pid} ${JSON.stringify(channel)}`) | |
+ this.outbox.sendAsyncMessage(AGENT_OUTBOX, { | |
+ type: "request", | |
+ requestID, | |
+ url, | |
+ scheme | |
+ }) | |
+ } | |
+ updateRequest(channel /*: Channel */, status /*: RequestStatus */) { | |
+ const { url, scheme, requestID } = channel | |
+ debug && console.log(`request${this.pid} ${JSON.stringify(channel)}`) | |
+ this.outbox.sendAsyncMessage(AGENT_OUTBOX, { | |
+ type: "requestUpdate", | |
+ requestID, | |
+ status, | |
+ url, | |
+ scheme | |
+ }) | |
+ } | |
+ head(head /*:Head*/) { | |
+ this.requests[head.requestID].head(head) | |
+ } | |
+ body(body /*:Body*/) { | |
+ this.requests[body.requestID].body(body) | |
+ } | |
+ end(end /*:End*/) { | |
+ this.requests[end.requestID].end(end) | |
+ } | |
+ receiveMessage({ data } /*: AgentInbox */) { | |
+ debug && | |
+ console.log(`Receive message at ${this.pid} ${JSON.stringify(data)}`) | |
+ | |
+ switch (data.type) { | |
+ case "terminate": | |
+ return this.terminate() | |
+ case "unregister": | |
+ return this.unregister(data) | |
+ case "register": | |
+ return this.register(data) | |
+ case "head": | |
+ return this.head(data) | |
+ case "body": | |
+ return this.body(data) | |
+ case "end": | |
+ return this.end(data) | |
+ } | |
+ } | |
+ | |
+ terminate() { | |
+ debug && console.log(`Terminate ${this.pid}`) | |
+ | |
+ const { protocols, requests } = this | |
+ this.inbox.removeMessageListener(AGENT_INBOX, this) | |
+ | |
+ delete this.protocols | |
+ delete this.outbox | |
+ delete this.inbox | |
+ | |
+ for (const requestID in requests) { | |
+ const request = requests[requestID] | |
+ request.abort() | |
+ } | |
+ | |
+ for (const scheme in protocols) { | |
+ unregisterProtocol(scheme) | |
+ } | |
+ } | |
+ | |
+ static new() { | |
+ const self = new Agent() | |
+ debug && console.log(`new ${self.pid}`) | |
+ | |
+ self.inbox.addMessageListener(AGENT_INBOX, self) | |
+ | |
+ const protocols /*: { [string]: ProtocolSpec } */ = | |
+ cpmm.initialProcessData[PROTOCOLS] | |
+ console.log(`Initial protocols ${JSON.stringify(protocols)}`) | |
+ | |
+ if (protocols) { | |
+ for (let scheme in protocols) { | |
+ self.register(protocols[scheme]) | |
+ } | |
+ } | |
+ } | |
+} | |
+ | |
+if (!isParent) { | |
+ Agent.new() | |
+} | |
+ | |
+const ExtensionProtocol = { Supervisor, Agent } | |
diff --git a/toolkit/components/extensions/child/ext-protocol.js b/toolkit/components/extensions/child/ext-protocol.js | |
new file mode 100644 | |
index 000000000000..550db7991cd4 | |
--- /dev/null | |
+++ b/toolkit/components/extensions/child/ext-protocol.js | |
@@ -0,0 +1,235 @@ | |
+"use strict" | |
+ | |
+ChromeUtils.defineModuleGetter( | |
+ this, | |
+ "Services", | |
+ "resource://gre/modules/Services.jsm" | |
+) | |
+ | |
+const OUTBOX = "libdweb:protocol:handler:outbox" | |
+const INBOX = "libdweb:protocol:handler:inbox" | |
+ | |
+const ACTIVE = Cr.NS_OK | |
+const PAUSED = Cr.NS_BASE_STREAM_WOULD_BLOCK | |
+const CLOSED = Cr.NS_BASE_STREAM_CLOSED | |
+const ABORTED = Cr.NS_BINDING_ABORTED | |
+ | |
+/*:: | |
+import type { HandlerInbox, RequestStatus, HandlerOutbox, Inn, Out } from "./router" | |
+ | |
+interface Head { | |
+ contentType?: string, | |
+ contentLength?: number, | |
+ contentCharset?: string | |
+} | |
+ | |
+interface Body { | |
+ content: AsyncIterator<ArrayBuffer> | |
+} | |
+ | |
+interface Response extends Head, Body { | |
+} | |
+ | |
+interface Handler { | |
+ ({ url: string }):Response|Promise<Response> | |
+} | |
+ | |
+type Status = | |
+ | RequestStatus | |
+ | typeof Cr.NS_BASE_STREAM_CLOSED | |
+ | |
+interface ConnectionManager { | |
+ disconnect(id:string):void; | |
+} | |
+ | |
+interface Client { | |
+ +protocol: { | |
+ registerProtocol(string, Handler): Promise<void> | |
+ } | |
+} | |
+*/ | |
+ | |
+class Connection { | |
+ /*:: | |
+ requestID:string | |
+ port:Out<HandlerOutbox> | |
+ content:AsyncIterator<ArrayBuffer> | |
+ status:Status | |
+ */ | |
+ constructor( | |
+ requestID /*:string*/, | |
+ port /*:Out<HandlerOutbox>*/, | |
+ content /*:AsyncIterator<ArrayBuffer>*/ | |
+ ) { | |
+ this.requestID = requestID | |
+ this.port = port | |
+ this.content = content | |
+ this.status = ACTIVE | |
+ } | |
+ head({ contentType, contentCharset, contentLength } /*:Head*/) { | |
+ this.port.sendAsyncMessage(OUTBOX, { | |
+ type: "head", | |
+ requestID: this.requestID, | |
+ contentType, | |
+ contentCharset, | |
+ contentLength | |
+ }) | |
+ } | |
+ body(content /*:ArrayBuffer*/) { | |
+ this.port.sendAsyncMessage(OUTBOX, { | |
+ type: "body", | |
+ requestID: this.requestID, | |
+ content | |
+ }) | |
+ } | |
+ end(status = 0) { | |
+ this.port.sendAsyncMessage(OUTBOX, { | |
+ type: "end", | |
+ requestID: this.requestID, | |
+ status | |
+ }) | |
+ } | |
+ suspend(manager /*:ConnectionManager*/) { | |
+ if (this.status === ACTIVE) { | |
+ this.status = PAUSED | |
+ } | |
+ return this | |
+ } | |
+ async resume(manager /*:ConnectionManager*/) { | |
+ while (this.status === ACTIVE) { | |
+ const { done, value } = await this.content.next() | |
+ if (this.status === ACTIVE) { | |
+ if (value) { | |
+ this.body(value) | |
+ } | |
+ | |
+ if (done) { | |
+ this.close(manager) | |
+ } | |
+ } | |
+ } | |
+ } | |
+ close(manager) { | |
+ manager.disconnect(this.requestID) | |
+ this.status = CLOSED | |
+ this.end(0) | |
+ delete this.port | |
+ } | |
+ abort(manager /*:ConnectionManager*/) { | |
+ manager.disconnect(this.requestID) | |
+ this.status = ABORTED | |
+ delete this.port | |
+ } | |
+} | |
+ | |
+class Protocol /*::implements ConnectionManager*/ { | |
+ /*:: | |
+ context: BaseContext | |
+ handlers: { [string]: Handler } | |
+ outbox: Out<HandlerOutbox> | |
+ inbox: Inn<HandlerInbox> | |
+ connections: {[string]: Connection} | |
+ */ | |
+ constructor(context /*: BaseContext */) { | |
+ this.context = context | |
+ this.handlers = {} | |
+ this.connections = {} | |
+ this.outbox = context.childManager.messageManager | |
+ this.inbox = context.messageManager | |
+ } | |
+ receiveMessage({ name, data, target } /*: HandlerInbox */) { | |
+ console.log(`Receive message Addon.Child ${name} ${JSON.stringify(data)}`) | |
+ switch (data.type) { | |
+ case `request`: { | |
+ return void this.request(data, target) | |
+ } | |
+ case `requestUpdate`: { | |
+ return this.updateRequest(data) | |
+ } | |
+ } | |
+ } | |
+ register(scheme /*: string */, handler /*: Handler */) { | |
+ this.handlers[scheme] = handler | |
+ console.log(`register "${scheme}" protocol handler: ${String(handler)}`) | |
+ this.outbox.sendAsyncMessage(OUTBOX, { | |
+ type: "install", | |
+ scheme | |
+ }) | |
+ } | |
+ async request(request, target /*: Out<HandlerOutbox> */) { | |
+ const { requestID, scheme, url } = request | |
+ const handler = this.handlers[request.scheme] | |
+ try { | |
+ delete request.requestID | |
+ const promise = Reflect.apply(handler, null, [ | |
+ Cu.cloneInto(request, handler) | |
+ ]) | |
+ const response = Cu.waiveXrays(await promise) | |
+ const connection = new Connection(requestID, target, response.content) | |
+ this.connect(connection) | |
+ connection.head(response) | |
+ connection.resume(this) | |
+ } catch (error) { | |
+ target.sendAsyncMessage(OUTBOX, { | |
+ type: "end", | |
+ requestID, | |
+ status: Cr.NS_ERROR_XPC_JAVASCRIPT_ERROR | |
+ }) | |
+ } | |
+ } | |
+ connect(connection /*:Connection*/) { | |
+ this.connections[connection.requestID] = connection | |
+ } | |
+ disconnect(requestID /*:string*/) { | |
+ delete this.connections[requestID] | |
+ } | |
+ updateRequest(data) { | |
+ const { requestID, status } = data | |
+ const connection = this.connections[requestID] | |
+ if (connection) { | |
+ switch (status) { | |
+ case ACTIVE: { | |
+ return void connection.resume(this) | |
+ } | |
+ case PAUSED: { | |
+ return void connection.suspend(this) | |
+ } | |
+ case ABORTED: { | |
+ return void connection.abort(this) | |
+ } | |
+ default: | |
+ return void this | |
+ } | |
+ } else { | |
+ console.error(`Received update for the a closed connection ${requestID}`) | |
+ } | |
+ } | |
+ | |
+ static spawn(context) { | |
+ const self = new Protocol(context) | |
+ self.inbox.addMessageListener(INBOX, self) | |
+ | |
+ return self | |
+ } | |
+} | |
+ | |
+class ProtocolClient extends ExtensionAPI /*::<Client>*/ { | |
+ getAPI(context) { | |
+ const init = context.childManager.callParentAsyncFunction( | |
+ "protocol.spawn", | |
+ [] | |
+ ) | |
+ | |
+ const protocol = Protocol.spawn(context) | |
+ | |
+ return { | |
+ protocol: { | |
+ async registerProtocol(scheme, handler) { | |
+ await init | |
+ protocol.register(scheme, handler) | |
+ } | |
+ } | |
+ } | |
+ } | |
+} | |
+this.protocol = ProtocolClient | |
diff --git a/toolkit/components/extensions/jar.mn b/toolkit/components/extensions/jar.mn | |
index d637cc3ddae3..5f53a3ff631f 100644 | |
--- a/toolkit/components/extensions/jar.mn | |
+++ b/toolkit/components/extensions/jar.mn | |
@@ -41,6 +41,7 @@ toolkit.jar: | |
content/extensions/parent/ext-userScripts.js (parent/ext-userScripts.js) | |
content/extensions/parent/ext-webRequest.js (parent/ext-webRequest.js) | |
content/extensions/parent/ext-webNavigation.js (parent/ext-webNavigation.js) | |
+ content/extensions/parent/ext-protocol.js (parent/ext-protocol.js) | |
content/extensions/child/ext-backgroundPage.js (child/ext-backgroundPage.js) | |
content/extensions/child/ext-contentScripts.js (child/ext-contentScripts.js) | |
content/extensions/child/ext-extension.js (child/ext-extension.js) | |
@@ -54,3 +55,4 @@ toolkit.jar: | |
content/extensions/child/ext-userScripts.js (child/ext-userScripts.js) | |
content/extensions/child/ext-userScripts-content.js (child/ext-userScripts-content.js) | |
content/extensions/child/ext-webRequest.js (child/ext-webRequest.js) | |
+ content/extensions/child/ext-protocol.js (child/ext-protocol.js) | |
diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build | |
index 573663d20701..0106e0de0050 100644 | |
--- a/toolkit/components/extensions/moz.build | |
+++ b/toolkit/components/extensions/moz.build | |
@@ -34,6 +34,7 @@ EXTRA_JS_MODULES += [ | |
'PerformanceCounters.jsm', | |
'ProxyScriptContext.jsm', | |
'Schemas.jsm', | |
+ 'ExtensionProtocol.jsm', | |
] | |
if CONFIG['MOZ_WIDGET_TOOLKIT'] != "android": | |
diff --git a/toolkit/components/extensions/parent/ext-protocol.js b/toolkit/components/extensions/parent/ext-protocol.js | |
new file mode 100644 | |
index 000000000000..20dddff6650b | |
--- /dev/null | |
+++ b/toolkit/components/extensions/parent/ext-protocol.js | |
@@ -0,0 +1,34 @@ | |
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ | |
+/* vim: set sts=2 sw=2 et tw=80: */ | |
+"use strict" | |
+ | |
+Cu.importGlobalProperties(["URL"]) | |
+ | |
+ChromeUtils.defineModuleGetter( | |
+ this, | |
+ "ExtensionProtocol", | |
+ "resource://gre/modules/ExtensionProtocol.jsm" | |
+) | |
+ | |
+this.protocol = class ProtocolHost extends ExtensionAPI /*::<Host>*/ { | |
+ /*:: | |
+ supervisor:Supervisor | |
+ */ | |
+ onStartup() { | |
+ const load /*:any*/ = Cu.import | |
+ this.supervisor = ExtensionProtocol.Supervisor.new() | |
+ } | |
+ onShutdown(reason) { | |
+ if (this.supervisor) { | |
+ this.supervisor.terminate() | |
+ delete this.supervisor | |
+ } | |
+ } | |
+ getAPI(context) { | |
+ return { | |
+ protocol: { | |
+ spawn() {} | |
+ } | |
+ } | |
+ } | |
+} | |
diff --git a/toolkit/components/extensions/schemas/jar.mn b/toolkit/components/extensions/schemas/jar.mn | |
index 5cbf40ea3609..d3c9553c26b4 100644 | |
--- a/toolkit/components/extensions/schemas/jar.mn | |
+++ b/toolkit/components/extensions/schemas/jar.mn | |
@@ -43,3 +43,4 @@ toolkit.jar: | |
content/extensions/schemas/user_scripts_content.json | |
content/extensions/schemas/web_navigation.json | |
content/extensions/schemas/web_request.json | |
+ content/extensions/schemas/protocol.json | |
\ No newline at end of file | |
diff --git a/toolkit/components/extensions/schemas/protocol.json b/toolkit/components/extensions/schemas/protocol.json | |
new file mode 100644 | |
index 000000000000..e11f5b177829 | |
--- /dev/null | |
+++ b/toolkit/components/extensions/schemas/protocol.json | |
@@ -0,0 +1,111 @@ | |
+[ | |
+ { | |
+ "namespace": "manifest", | |
+ "types": [ | |
+ { | |
+ "id": "ProtocolService", | |
+ "type": "object", | |
+ "description": "Represents a protocol service registration.", | |
+ "properties": { | |
+ "name": { | |
+ "description": "A user-readable title string for the protocol handler. This will be displayed to the user in interface objects as needed.", | |
+ "type": "string" | |
+ }, | |
+ "protocol": { | |
+ "description": "The protocol the site wishes to handle, specified as a string. For example, you can register to handle SMS text message links by registering to handle the \"sms\" scheme.", | |
+ "choices": [{ | |
+ "type": "string", | |
+ "enum": [ | |
+ "bitcoin", "dat", "dweb", "geo", "gopher", "im", "ipfs", "ipns", "irc", "ircs", "magnet", | |
+ "mailto", "mms", "news", "nntp", "sip", "sms", "smsto", "ssb", "ssh", | |
+ "tel", "urn", "webcal", "wtai", "xmpp" | |
+ ] | |
+ }, { | |
+ "type": "string", | |
+ "pattern": "^(ext|web)\\+[a-z0-9.+-]+$" | |
+ }] | |
+ }, | |
+ "js": { | |
+ "description": "JS file implementing a service.", | |
+ "choices": [ | |
+ { | |
+ "type": "object", | |
+ "properties": { | |
+ "file": { | |
+ "$ref": "manifest.ExtensionURL" | |
+ } | |
+ } | |
+ } | |
+ ] | |
+ } | |
+ } | |
+ }, | |
+ { | |
+ "$extend": "WebExtensionManifest", | |
+ "properties": { | |
+ "protocol": { | |
+ "description": "A list of network protocol implementations.", | |
+ "optional": true, | |
+ "type": "array", | |
+ "items": {"$ref": "ProtocolService"} | |
+ } | |
+ } | |
+ } | |
+ ] | |
+ }, | |
+ { | |
+ "namespace": "protocol", | |
+ "description": "Network protocol implementation", | |
+ "manifest": ["protocol"], | |
+ "permissions": ["protocol"], | |
+ "types": [ | |
+ { | |
+ "id": "Request", | |
+ "type": "object", | |
+ "description": "An object passed to the protocol handler", | |
+ "properties": { | |
+ "url": { | |
+ "type": "string", | |
+ "description": "requested url" | |
+ } | |
+ } | |
+ }, | |
+ { | |
+ "id": "ProtocolHandler", | |
+ "type": "function", | |
+ "description": "Protocol handler", | |
+ "async": true, | |
+ "parameters": [ | |
+ { | |
+ "name": "request", | |
+ "$ref": "Request" | |
+ } | |
+ ] | |
+ } | |
+ ], | |
+ "functions": [ | |
+ { | |
+ "name": "registerProtocol", | |
+ "type": "function", | |
+ "description": "regitruesters network protocol handler", | |
+ "async": true, | |
+ "parameters": [ | |
+ { | |
+ "name": "scheme", | |
+ "type": "string", | |
+ "description": | |
+ "The protocol scheme that implementation will handle." | |
+ }, | |
+ { | |
+ "name": "handler", | |
+ "$ref": "ProtocolHandler", | |
+ "type": "function", | |
+ "description": "Protocol handler for the registered protocol" | |
+ } | |
+ ] | |
+ } | |
+ ], | |
+ "properties": {}, | |
+ "events": [] | |
+ } | |
+] | |
diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.ini b/toolkit/components/extensions/test/mochitest/mochitest-common.ini | |
index 940574a9c10d..88462cd294fb 100644 | |
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini | |
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini | |
@@ -100,6 +100,7 @@ skip-if = os == 'android' # Android does not support multiple windows. | |
skip-if = (verify && debug && (os == 'linux' || os == 'mac')) | |
[test_ext_notifications.html] | |
[test_ext_protocolHandlers.html] | |
+[test_ext_protocol_api.html] | |
skip-if = (toolkit == 'android') # bug 1342577 | |
[test_ext_redirect_jar.html] | |
[test_ext_runtime_connect.html] | |
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_protocol_api.html b/toolkit/components/extensions/test/mochitest/test_ext_protocol_api.html | |
new file mode 100644 | |
index 000000000000..38a490011209 | |
--- /dev/null | |
+++ b/toolkit/components/extensions/test/mochitest/test_ext_protocol_api.html | |
@@ -0,0 +1,355 @@ | |
+<!DOCTYPE HTML> | |
+<html> | |
+<head> | |
+ <meta charset="utf-8"> | |
+ <title>Test for protocol handlers</title> | |
+ <script src="/tests/SimpleTest/SimpleTest.js"></script> | |
+ <script src="/tests/SimpleTest/AddTask.js"></script> | |
+ <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> | |
+ <script type="text/javascript" src="head.js"></script> | |
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> | |
+</head> | |
+<body> | |
+ | |
+<script type="text/javascript"> | |
+"use strict"; | |
+ | |
+/* eslint-disable mozilla/balanced-listeners */ | |
+/* global addMessageListener, sendAsyncMessage */ | |
+ | |
+function protocolChromeScript() { | |
+ addMessageListener("setup", () => { | |
+ let data = {}; | |
+ // const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] | |
+ // .getService(Ci.nsIExternalProtocolService); | |
+ // let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo"); | |
+ // data.preferredAction = protoInfo.preferredAction === protoInfo.useHelperApp; | |
+ | |
+ // let handlers = protoInfo.possibleApplicationHandlers; | |
+ // data.handlers = handlers.length; | |
+ | |
+ // let handler = handlers.queryElementAt(0, Ci.nsIHandlerApp); | |
+ // data.isWebHandler = handler instanceof Ci.nsIWebHandlerApp; | |
+ // data.uriTemplate = handler.uriTemplate; | |
+ | |
+ // // ext+ protocols should be set as default when there is only one | |
+ // data.preferredApplicationHandler = protoInfo.preferredApplicationHandler == handler; | |
+ // data.alwaysAskBeforeHandling = protoInfo.alwaysAskBeforeHandling; | |
+ // const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"] | |
+ // .getService(Ci.nsIHandlerService); | |
+ // handlerSvc.store(protoInfo); | |
+ | |
+ sendAsyncMessage("handlerData", data); | |
+ }); | |
+} | |
+ | |
+// add_task(async function test_protocolHandler() { | |
+// await SpecialPowers.pushPrefEnv({set: [ | |
+// ["extensions.allowPrivateBrowsingByDefault", false], | |
+// ]}); | |
+// let extensionData = { | |
+// manifest: { | |
+// "protocol_handlers": [ | |
+// { | |
+// "protocol": "ext+foo", | |
+// "name": "a foo protocol handler", | |
+// "uriTemplate": "foo.html?val=%s", | |
+// }, | |
+// ], | |
+// }, | |
+ | |
+// background() { | |
+// browser.test.onMessage.addListener(async (msg, arg) => { | |
+// if (msg == "open") { | |
+// let tab = await browser.tabs.create({url: arg}); | |
+// browser.test.sendMessage("opened", tab.id); | |
+// } else if (msg == "close") { | |
+// await browser.tabs.remove(arg); | |
+// browser.test.sendMessage("closed"); | |
+// } | |
+// }); | |
+// browser.test.sendMessage("test-url", browser.runtime.getURL("foo.html")); | |
+// }, | |
+ | |
+// files: { | |
+// "foo.js": function() { | |
+// browser.test.sendMessage("test-query", location.search); | |
+// }, | |
+// "foo.html": `<!DOCTYPE html> | |
+// <html> | |
+// <head> | |
+// <meta charset="utf-8"> | |
+// <script src="foo.js"><\/script> | |
+// </head> | |
+// </html>`, | |
+// }, | |
+// }; | |
+ | |
+// let pb_extension = ExtensionTestUtils.loadExtension({ | |
+// background() { | |
+// browser.test.onMessage.addListener(async (msg, arg) => { | |
+// if (msg == "open") { | |
+// let tab = await browser.windows.create({url: arg, incognito: true}); | |
+// browser.test.sendMessage("opened", tab.id); | |
+// } else if (msg == "close") { | |
+// await browser.windows.remove(arg); | |
+// browser.test.sendMessage("closed"); | |
+// } | |
+// }); | |
+// }, | |
+// incognitoOverride: "spanning", | |
+// }); | |
+// await pb_extension.startup(); | |
+ | |
+// let extension = ExtensionTestUtils.loadExtension(extensionData); | |
+// await extension.startup(); | |
+// let handlerUrl = await extension.awaitMessage("test-url"); | |
+ | |
+// // Ensure that the protocol handler is configured, and set it as default to | |
+// // bypass the dialog. | |
+// let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript); | |
+ | |
+// let msg = chromeScript.promiseOneMessage("handlerData"); | |
+// chromeScript.sendAsyncMessage("setup"); | |
+// let data = await msg; | |
+// ok(data.preferredAction, "using a helper application is the preferred action"); | |
+// ok(data.preferredApplicationHandler, "handler was set as default handler"); | |
+// is(data.handlers, 1, "one handler is set"); | |
+// ok(!data.alwaysAskBeforeHandling, "will not show dialog"); | |
+// ok(data.isWebHandler, "the handler is a web handler"); | |
+// is(data.uriTemplate, `${handlerUrl}?val=%s`, "correct url template"); | |
+// chromeScript.destroy(); | |
+ | |
+// extension.sendMessage("open", "ext+foo:test"); | |
+// let id = await extension.awaitMessage("opened"); | |
+ | |
+// let query = await extension.awaitMessage("test-query"); | |
+// is(query, "?val=ext%2Bfoo%3Atest", "test query ok"); | |
+ | |
+// extension.sendMessage("close", id); | |
+// await extension.awaitMessage("closed"); | |
+ | |
+// // Test the protocol in a private window, watch for the | |
+// // console error. | |
+// consoleMonitor.start([{message: /NS_ERROR_FILE_NOT_FOUND/}]); | |
+ | |
+// // Expect the chooser window to be open, close it. | |
+// chromeScript = SpecialPowers.loadChromeScript(async () => { | |
+// const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); | |
+ | |
+// let window = await BrowserTestUtils.domWindowOpened(undefined, win => { | |
+// return BrowserTestUtils.waitForEvent(win, "load", false, event => { | |
+// let win = event.target.defaultView; | |
+// return win.document.documentElement.getAttribute("id") === "handling"; | |
+// }); | |
+// }); | |
+// let entry = window.document.getElementById("items").firstChild; | |
+// sendAsyncMessage("handling", {name: entry.getAttribute("name"), disabled: entry.disabled}); | |
+// window.close(); | |
+// }); | |
+ | |
+// let testData = chromeScript.promiseOneMessage("handling"); | |
+// pb_extension.sendMessage("open", "ext+foo:test"); | |
+// id = await pb_extension.awaitMessage("opened"); | |
+// await consoleMonitor.finished(); | |
+// let entry = await testData; | |
+// is(entry.name, "a foo protocol handler", "entry is correct"); | |
+// ok(entry.disabled, "handler is disabled"); | |
+ | |
+// pb_extension.sendMessage("close", id); | |
+// await pb_extension.awaitMessage("closed"); | |
+// await pb_extension.unload(); | |
+ | |
+// // Shutdown the addon, then ensure the protocol was removed. | |
+// await extension.unload(); | |
+// chromeScript = SpecialPowers.loadChromeScript(() => { | |
+// addMessageListener("setup", () => { | |
+// const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] | |
+// .getService(Ci.nsIExternalProtocolService); | |
+// let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo"); | |
+// sendAsyncMessage("preferredApplicationHandler", !protoInfo.preferredApplicationHandler); | |
+// let handlers = protoInfo.possibleApplicationHandlers; | |
+ | |
+// sendAsyncMessage("handlerData", { | |
+// preferredApplicationHandler: !protoInfo.preferredApplicationHandler, | |
+// handlers: handlers.length, | |
+// }); | |
+// }); | |
+// }); | |
+ | |
+// msg = chromeScript.promiseOneMessage("handlerData"); | |
+// chromeScript.sendAsyncMessage("setup"); | |
+// data = await msg; | |
+// ok(data.preferredApplicationHandler, "no preferred handler is set"); | |
+// is(data.handlers, 0, "no handler is set"); | |
+// chromeScript.destroy(); | |
+// }); | |
+ | |
+add_task(async function test_protocolHandler_two() { | |
+ let extensionData = { | |
+ manifest: { | |
+ "protocol": [ | |
+ { | |
+ "name": "a foo protocol handler", | |
+ "protocol": "ext+foo", | |
+ "js": { | |
+ "file": "./protocol.js" | |
+ } | |
+ } | |
+ ] | |
+ }, | |
+ files: { | |
+ "protocol.js": function() { | |
+ browser.test.assertTrue(false, "Boom!") | |
+ } | |
+ } | |
+ }; | |
+ | |
+ let extension = ExtensionTestUtils.loadExtension(extensionData); | |
+ await extension.startup(); | |
+ | |
+ // Ensure that the protocol handler is configured, and set it as default, | |
+ // but because there are two handlers, the dialog is not bypassed. We | |
+ // don't test the actual dialog ui, it's been here forever and works based | |
+ // on the alwaysAskBeforeHandling value. | |
+ let chromeScript = SpecialPowers.loadChromeScript(protocolChromeScript); | |
+ | |
+ let msg = chromeScript.promiseOneMessage("handlerData"); | |
+ chromeScript.sendAsyncMessage("setup"); | |
+ let data = await msg; | |
+ // ok(data.preferredAction, "using a helper application is the preferred action"); | |
+ // ok(data.preferredApplicationHandler, "preferred handler is set"); | |
+ // is(data.handlers, 2, "two handlers are set"); | |
+ // ok(data.alwaysAskBeforeHandling, "will show dialog"); | |
+ // ok(data.isWebHandler, "the handler is a web handler"); | |
+ chromeScript.destroy(); | |
+ await extension.unload(); | |
+}); | |
+ | |
+// add_task(async function test_protocolHandler_https_target() { | |
+// let extensionData = { | |
+// manifest: { | |
+// "protocol_handlers": [ | |
+// { | |
+// "protocol": "ext+foo", | |
+// "name": "http target", | |
+// "uriTemplate": "https://example.com/foo.html?val=%s", | |
+// }, | |
+// ], | |
+// }, | |
+// }; | |
+ | |
+// let extension = ExtensionTestUtils.loadExtension(extensionData); | |
+// await extension.startup(); | |
+// ok(true, "https uriTemplate target works"); | |
+// await extension.unload(); | |
+// }); | |
+ | |
+// add_task(async function test_protocolHandler_http_target() { | |
+// let extensionData = { | |
+// manifest: { | |
+// "protocol_handlers": [ | |
+// { | |
+// "protocol": "ext+foo", | |
+// "name": "http target", | |
+// "uriTemplate": "http://example.com/foo.html?val=%s", | |
+// }, | |
+// ], | |
+// }, | |
+// }; | |
+ | |
+// let extension = ExtensionTestUtils.loadExtension(extensionData); | |
+// await extension.startup(); | |
+// ok(true, "http uriTemplate target works"); | |
+// await extension.unload(); | |
+// }); | |
+ | |
+// add_task(async function test_protocolHandler_restricted_protocol() { | |
+// let extensionData = { | |
+// manifest: { | |
+// "protocol_handlers": [ | |
+// { | |
+// "protocol": "http", | |
+// "name": "take over the http protocol", | |
+// "uriTemplate": "http.html?val=%s", | |
+// }, | |
+// ], | |
+// }, | |
+// }; | |
+ | |
+// consoleMonitor.start([{message: /processing protocol_handlers\.0\.protocol/}]); | |
+ | |
+// let extension = ExtensionTestUtils.loadExtension(extensionData); | |
+// await Assert.rejects(extension.startup(), | |
+// /startup failed/, | |
+// "unable to register restricted handler protocol"); | |
+ | |
+// await consoleMonitor.finished(); | |
+// }); | |
+ | |
+// add_task(async function test_protocolHandler_restricted_uriTemplate() { | |
+// let extensionData = { | |
+// manifest: { | |
+// "protocol_handlers": [ | |
+// { | |
+// "protocol": "ext+foo", | |
+// "name": "take over the http protocol", | |
+// "uriTemplate": "ftp://example.com/file.txt", | |
+// }, | |
+// ], | |
+// }, | |
+// }; | |
+ | |
+// consoleMonitor.start([{message: /processing protocol_handlers\.0\.uriTemplate/}]); | |
+ | |
+// let extension = ExtensionTestUtils.loadExtension(extensionData); | |
+// await Assert.rejects(extension.startup(), | |
+// /startup failed/, | |
+// "unable to register restricted handler uriTemplate"); | |
+ | |
+// await consoleMonitor.finished(); | |
+// }); | |
+ | |
+// add_task(async function test_protocolHandler_duplicate() { | |
+// let extensionData = { | |
+// manifest: { | |
+// "protocol_handlers": [ | |
+// { | |
+// "protocol": "ext+foo", | |
+// "name": "foo protocol", | |
+// "uriTemplate": "foo.html?val=%s", | |
+// }, | |
+// { | |
+// "protocol": "ext+foo", | |
+// "name": "foo protocol", | |
+// "uriTemplate": "foo.html?val=%s", | |
+// }, | |
+// ], | |
+// }, | |
+// }; | |
+ | |
+// let extension = ExtensionTestUtils.loadExtension(extensionData); | |
+// await extension.startup(); | |
+ | |
+// // Get the count of handlers installed. | |
+// let chromeScript = SpecialPowers.loadChromeScript(() => { | |
+// addMessageListener("setup", () => { | |
+// const protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"] | |
+// .getService(Ci.nsIExternalProtocolService); | |
+// let protoInfo = protoSvc.getProtocolHandlerInfo("ext+foo"); | |
+// let handlers = protoInfo.possibleApplicationHandlers; | |
+// sendAsyncMessage("handlerData", handlers.length); | |
+// }); | |
+// }); | |
+ | |
+// let msg = chromeScript.promiseOneMessage("handlerData"); | |
+// chromeScript.sendAsyncMessage("setup"); | |
+// let data = await msg; | |
+// is(data, 1, "cannot re-register the same handler config"); | |
+// chromeScript.destroy(); | |
+// await extension.unload(); | |
+// }); | |
+</script> | |
+ | |
+</body> | |
+</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment