/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ ChromeUtils.defineESModuleGetters(this, { WebChannel: "resource://gre/modules/WebChannel.sys.mjs", }); const HTTP_PATH = "http://example.com"; const HTTP_ENDPOINT = getRootDirectory(gTestPath).replace("chrome://mochitests/content", "") + "file_web_channel.html"; const HTTP_MISMATCH_PATH = "http://example.org"; const HTTP_IFRAME_PATH = "http://mochi.test:8888"; const HTTP_REDIRECTED_IFRAME_PATH = "http://example.org"; requestLongerTimeout(2); // timeouts in debug builds. // Keep this synced with /mobile/android/tests/browser/robocop/testWebChannel.js // as much as possible. (We only have that since we can't run browser chrome // tests on Android. Yet?) var gTests = [ { desc: "WebChannel generic message", run() { return new Promise(function(resolve, reject) { let tab; let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH)); channel.listen(function(id, message, target) { is(id, "generic"); is(message.something.nested, "hello"); channel.stopListening(); gBrowser.removeTab(tab); resolve(); }); tab = BrowserTestUtils.addTab( gBrowser, HTTP_PATH + HTTP_ENDPOINT + "?generic" ); }); }, }, { desc: "WebChannel generic message in a private window.", async run() { let promiseTestDone = new Promise(function(resolve, reject) { let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH)); channel.listen(function(id, message, target) { is(id, "generic"); is(message.something.nested, "hello"); channel.stopListening(); resolve(); }); }); const url = HTTP_PATH + HTTP_ENDPOINT + "?generic"; let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ private: true, }); await BrowserTestUtils.openNewForegroundTab(privateWindow.gBrowser, url); await promiseTestDone; await BrowserTestUtils.closeWindow(privateWindow); }, }, { desc: "WebChannel two way communication", run() { return new Promise(function(resolve, reject) { let tab; let channel = new WebChannel("twoway", Services.io.newURI(HTTP_PATH)); channel.listen(function(id, message, sender) { is(id, "twoway", "bad id"); ok(message.command, "command not ok"); if (message.command === "one") { channel.send({ data: { nested: true } }, sender); } if (message.command === "two") { is(message.detail.data.nested, true); channel.stopListening(); gBrowser.removeTab(tab); resolve(); } }); tab = BrowserTestUtils.addTab( gBrowser, HTTP_PATH + HTTP_ENDPOINT + "?twoway" ); }); }, }, { desc: "WebChannel two way communication in an iframe", async run() { let parentChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH)); let iframeChannel = new WebChannel( "twoway", Services.io.newURI(HTTP_IFRAME_PATH) ); let promiseTestDone = new Promise(function(resolve, reject) { parentChannel.listen(function(id, message, sender) { reject(new Error("WebChannel message incorrectly sent to parent")); }); iframeChannel.listen(function(id, message, sender) { is(id, "twoway", "bad id (2)"); ok(message.command, "command not ok (2)"); if (message.command === "one") { iframeChannel.send({ data: { nested: true } }, sender); } if (message.command === "two") { is(message.detail.data.nested, true); resolve(); } }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?iframe", }, async function() { await promiseTestDone; parentChannel.stopListening(); iframeChannel.stopListening(); } ); }, }, { desc: "WebChannel response to a redirected iframe", async run() { /** * This test checks that WebChannel responses are only sent * to an iframe if the iframe has not redirected to another origin. * Test flow: * 1. create a page, embed an iframe on origin A. * 2. the iframe sends a message `redirecting`, then redirects to * origin B. * 3. the iframe at origin B is set up to echo any messages back to the * test parent. * 4. the test parent receives the `redirecting` message from origin A. * the test parent creates a new channel with origin B. * 5. when origin B is ready, it sends a `loaded` message to the test * parent, letting the test parent know origin B is ready to echo * messages. * 5. the test parent tries to send a response to origin A. If the * WebChannel does not perform a valid origin check, the response * will be received by origin B. If the WebChannel does perform * a valid origin check, the response will not be sent. * 6. the test parent sends a `done` message to origin B, which origin * B echoes back. If the response to origin A is not echoed but * the message to origin B is, then hooray, the test passes. */ let preRedirectChannel = new WebChannel( "pre_redirect", Services.io.newURI(HTTP_IFRAME_PATH) ); let postRedirectChannel = new WebChannel( "post_redirect", Services.io.newURI(HTTP_REDIRECTED_IFRAME_PATH) ); let promiseTestDone = new Promise(function(resolve, reject) { preRedirectChannel.listen(function(id, message, preRedirectSender) { if (message.command === "redirecting") { postRedirectChannel.listen(function( aId, aMessage, aPostRedirectSender ) { is(aId, "post_redirect"); isnot(aMessage.command, "no_response_expected"); if (aMessage.command === "loaded") { // The message should not be received on the preRedirectChannel // because the target window has redirected. preRedirectChannel.send( { command: "no_response_expected" }, preRedirectSender ); postRedirectChannel.send( { command: "done" }, aPostRedirectSender ); } else if (aMessage.command === "done") { resolve(); } else { reject(new Error(`Unexpected command ${aMessage.command}`)); } }); } else { reject(new Error(`Unexpected command ${message.command}`)); } }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?iframe_pre_redirect", }, async function() { await promiseTestDone; preRedirectChannel.stopListening(); postRedirectChannel.stopListening(); } ); }, }, { desc: "WebChannel multichannel", run() { return new Promise(function(resolve, reject) { let tab; let channel = new WebChannel( "multichannel", Services.io.newURI(HTTP_PATH) ); channel.listen(function(id, message, sender) { is(id, "multichannel"); gBrowser.removeTab(tab); resolve(); }); tab = BrowserTestUtils.addTab( gBrowser, HTTP_PATH + HTTP_ENDPOINT + "?multichannel" ); }); }, }, { desc: "WebChannel unsolicited send, using system principal", async run() { let channel = new WebChannel("echo", Services.io.newURI(HTTP_PATH)); // an unsolicted message is sent from Chrome->Content which is then // echoed back. If the echo is received here, then the content // received the message. let messagePromise = new Promise(function(resolve, reject) { channel.listen(function(id, message, sender) { is(id, "echo"); is(message.command, "unsolicited"); resolve(); }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited", }, async function(targetBrowser) { channel.send( { command: "unsolicited" }, { browsingContext: targetBrowser.browsingContext, principal: Services.scriptSecurityManager.getSystemPrincipal(), } ); await messagePromise; channel.stopListening(); } ); }, }, { desc: "WebChannel unsolicited send, using target origin's principal", async run() { let targetURI = Services.io.newURI(HTTP_PATH); let channel = new WebChannel("echo", targetURI); // an unsolicted message is sent from Chrome->Content which is then // echoed back. If the echo is received here, then the content // received the message. let messagePromise = new Promise(function(resolve, reject) { channel.listen(function(id, message, sender) { is(id, "echo"); is(message.command, "unsolicited"); resolve(); }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited", }, async function(targetBrowser) { channel.send( { command: "unsolicited" }, { browsingContext: targetBrowser.browsingContext, principal: Services.scriptSecurityManager.createContentPrincipal( targetURI, {} ), } ); await messagePromise; channel.stopListening(); } ); }, }, { desc: "WebChannel unsolicited send with principal mismatch", async run() { let targetURI = Services.io.newURI(HTTP_PATH); let channel = new WebChannel("echo", targetURI); // two unsolicited messages are sent from Chrome->Content. The first, // `unsolicited_no_response_expected` is sent to the wrong principal // and should not be echoed back. The second, `done`, is sent to the // correct principal and should be echoed back. let messagePromise = new Promise(function(resolve, reject) { channel.listen(function(id, message, sender) { is(id, "echo"); if (message.command === "done") { resolve(); } else { reject(new Error(`Unexpected command ${message.command}`)); } }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?unsolicited", }, async function(targetBrowser) { let mismatchURI = Services.io.newURI(HTTP_MISMATCH_PATH); let mismatchPrincipal = Services.scriptSecurityManager.createContentPrincipal( mismatchURI, {} ); // send a message to the wrong principal. It should not be delivered // to content, and should not be echoed back. channel.send( { command: "unsolicited_no_response_expected" }, { browsingContext: targetBrowser.browsingContext, principal: mismatchPrincipal, } ); let targetPrincipal = Services.scriptSecurityManager.createContentPrincipal( targetURI, {} ); // send the `done` message to the correct principal. It // should be echoed back. channel.send( { command: "done" }, { browsingContext: targetBrowser.browsingContext, principal: targetPrincipal, } ); await messagePromise; channel.stopListening(); } ); }, }, { desc: "WebChannel non-window target", async run() { /** * This test ensures messages can be received from and responses * sent to non-window elements. * * First wait for the non-window element to send a "start" message. * Then send the non-window element a "done" message. * The non-window element will echo the "done" message back, if it * receives the message. * Listen for the response. If received, good to go! */ let channel = new WebChannel( "not_a_window", Services.io.newURI(HTTP_PATH) ); let testDonePromise = new Promise(function(resolve, reject) { channel.listen(function(id, message, sender) { if (message.command === "start") { channel.send({ command: "done" }, sender); } else if (message.command === "done") { resolve(); } else { reject(new Error(`Unexpected command ${message.command}`)); } }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?bubbles", }, async function() { await testDonePromise; channel.stopListening(); } ); }, }, { desc: "WebChannel disallows non-string message from non-whitelisted origin", async run() { /** * This test ensures that non-string messages can't be sent via WebChannels. * We create a page (on a non-whitelisted origin) which should send us two * messages immediately. The first message has an object for it's detail, * and the second has a string. We check that we only get the second * message. */ let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH)); let testDonePromise = new Promise((resolve, reject) => { channel.listen((id, message, sender) => { is(id, "objects"); is(message.type, "string"); resolve(); }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?object", }, async function() { await testDonePromise; channel.stopListening(); } ); }, }, { desc: "WebChannel allows both string and non-string message from whitelisted origin", async run() { /** * Same process as above, but we whitelist the origin before loading the page, * and expect to get *both* messages back (each exactly once). */ let channel = new WebChannel("objects", Services.io.newURI(HTTP_PATH)); let testDonePromise = new Promise((resolve, reject) => { let sawObject = false; let sawString = false; channel.listen((id, message, sender) => { is(id, "objects"); if (message.type === "object") { ok(!sawObject); sawObject = true; } else if (message.type === "string") { ok(!sawString); sawString = true; } else { reject(new Error(`Unknown message type: ${message.type}`)); } if (sawObject && sawString) { resolve(); } }); }); const webchannelWhitelistPref = "webchannel.allowObject.urlWhitelist"; let origWhitelist = Services.prefs.getCharPref(webchannelWhitelistPref); let newWhitelist = origWhitelist + " " + HTTP_PATH; Services.prefs.setCharPref(webchannelWhitelistPref, newWhitelist); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?object", }, async function() { await testDonePromise; Services.prefs.setCharPref(webchannelWhitelistPref, origWhitelist); channel.stopListening(); } ); }, }, { desc: "WebChannel errors handling the message are delivered back to content", async run() { const ERRNO_UNKNOWN_ERROR = 999; // WebChannel.sys.mjs doesn't export this. // The channel where we purposely fail responding to a command. let channel = new WebChannel("error", Services.io.newURI(HTTP_PATH)); // The channel where we see the response when the content sees the error let echoChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH)); let testDonePromise = new Promise((resolve, reject) => { // listen for the confirmation that content saw the error. echoChannel.listen((id, message, sender) => { is(id, "echo"); is(message.error, "oh no"); is(message.errno, ERRNO_UNKNOWN_ERROR); resolve(); }); // listen for a message telling us to simulate an error. channel.listen((id, message, sender) => { is(id, "error"); is(message.command, "oops"); throw new Error("oh no"); }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?error_thrown", }, async function() { await testDonePromise; channel.stopListening(); echoChannel.stopListening(); } ); }, }, { desc: "WebChannel errors due to an invalid channel are delivered back to content", async run() { const ERRNO_NO_SUCH_CHANNEL = 2; // WebChannel.sys.mjs doesn't export this. // The channel where we see the response when the content sees the error let echoChannel = new WebChannel("echo", Services.io.newURI(HTTP_PATH)); let testDonePromise = new Promise((resolve, reject) => { // listen for the confirmation that content saw the error. echoChannel.listen((id, message, sender) => { is(id, "echo"); is(message.error, "No Such Channel"); is(message.errno, ERRNO_NO_SUCH_CHANNEL); resolve(); }); }); await BrowserTestUtils.withNewTab( { gBrowser, url: HTTP_PATH + HTTP_ENDPOINT + "?error_invalid_channel", }, async function() { await testDonePromise; echoChannel.stopListening(); } ); }, }, ]; // gTests function test() { waitForExplicitFinish(); (async function() { await SpecialPowers.pushPrefEnv({ set: [["dom.security.https_first_pbm", false]], }); for (let testCase of gTests) { info("Running: " + testCase.desc); await testCase.run(); } })().then(finish, ex => { ok(false, "Unexpected Exception: " + ex); finish(); }); }