Skip to content

Instantly share code, notes, and snippets.

@Gozala
Created April 19, 2019 16:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gozala/857e8eecef43aa7407300f35856a88f9 to your computer and use it in GitHub Desktop.
Save Gozala/857e8eecef43aa7407300f35856a88f9 to your computer and use it in GitHub Desktop.
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