mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 878941 - Add IdP proxy code with tests. r=abr
This commit is contained in:
parent
a2007b5647
commit
defc49d86e
260
dom/media/IdpProxy.jsm
Normal file
260
dom/media/IdpProxy.jsm
Normal file
@ -0,0 +1,260 @@
|
||||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["IdpProxy"];
|
||||
|
||||
const {
|
||||
classes: Cc,
|
||||
interfaces: Ci,
|
||||
utils: Cu,
|
||||
results: Cr
|
||||
} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Sandbox",
|
||||
"resource://gre/modules/identity/Sandbox.jsm");
|
||||
|
||||
/**
|
||||
* An invisible iframe for hosting the idp shim.
|
||||
*
|
||||
* There is no visible UX here, as we assume the user has already
|
||||
* logged in elsewhere (on a different screen in the web site hosting
|
||||
* the RTC functions).
|
||||
*/
|
||||
function IdpChannel(uri, messageCallback) {
|
||||
this.sandbox = null;
|
||||
this.messagechannel = null;
|
||||
this.source = uri;
|
||||
this.messageCallback = messageCallback;
|
||||
}
|
||||
|
||||
IdpChannel.prototype = {
|
||||
/**
|
||||
* Create a hidden, sandboxed iframe for hosting the IdP's js shim.
|
||||
*
|
||||
* @param callback
|
||||
* (function) invoked when this completes, with an error
|
||||
* argument if there is a problem, no argument if everything is
|
||||
* ok
|
||||
*/
|
||||
open: function(callback) {
|
||||
if (this.sandbox) {
|
||||
return callback(new Error("IdP channel already open"));
|
||||
}
|
||||
|
||||
var ready = this._sandboxReady.bind(this, callback);
|
||||
this.sandbox = new Sandbox(this.source, ready);
|
||||
},
|
||||
|
||||
_sandboxReady: function(aCallback, aSandbox) {
|
||||
// Inject a message channel into the subframe.
|
||||
this.messagechannel = new aSandbox._frame.contentWindow.MessageChannel();
|
||||
try {
|
||||
Object.defineProperty(
|
||||
aSandbox._frame.contentWindow.wrappedJSObject,
|
||||
"rtcwebIdentityPort",
|
||||
{
|
||||
value: this.messagechannel.port2
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
this.close();
|
||||
aCallback(e); // oops, the IdP proxy overwrote this.. bad
|
||||
return;
|
||||
}
|
||||
this.messagechannel.port1.onmessage = function(msg) {
|
||||
this.messageCallback(msg.data);
|
||||
}.bind(this);
|
||||
this.messagechannel.port1.start();
|
||||
aCallback();
|
||||
},
|
||||
|
||||
send: function(msg) {
|
||||
this.messagechannel.port1.postMessage(msg);
|
||||
},
|
||||
|
||||
close: function IdpChannel_close() {
|
||||
if (this.sandbox) {
|
||||
if (this.messagechannel) {
|
||||
this.messagechannel.port1.close();
|
||||
}
|
||||
this.sandbox.free();
|
||||
}
|
||||
this.messagechannel = null;
|
||||
this.sandbox = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A message channel between the RTC PeerConnection and a designated IdP Proxy.
|
||||
*
|
||||
* @param domain (string) the domain to load up
|
||||
* @param protocol (string) Optional string for the IdP protocol
|
||||
*/
|
||||
function IdpProxy(domain, protocol) {
|
||||
IdpProxy.validateDomain(domain);
|
||||
IdpProxy.validateProtocol(protocol);
|
||||
|
||||
this.domain = domain;
|
||||
this.protocol = protocol || "default";
|
||||
|
||||
this._reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the domain is only a domain, and doesn't contain anything else.
|
||||
* Adds it to a URI, then checks that it matches perfectly.
|
||||
*/
|
||||
IdpProxy.validateDomain = function(domain) {
|
||||
let message = "Invalid domain for identity provider; ";
|
||||
if (!domain || typeof domain !== "string") {
|
||||
throw new Error(message + "must be a non-zero length string");
|
||||
}
|
||||
|
||||
message += "must only have a domain name and optionally a port";
|
||||
try {
|
||||
let ioService = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService);
|
||||
let uri = ioService.newURI('https://' + domain + '/', null, null);
|
||||
|
||||
// this should trap errors
|
||||
// we could check uri.userPass, uri.path and uri.ref, but there is no need
|
||||
if (uri.hostPort !== domain) {
|
||||
throw new Error(message);
|
||||
}
|
||||
} catch (e if (e.result === Cr.NS_ERROR_MALFORMED_URI)) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks that the IdP protocol is sane. In particular, we don't want someone
|
||||
* adding relative paths (e.g., "../../myuri"), which could be used to move
|
||||
* outside of /.well-known/ and into space that they control.
|
||||
*/
|
||||
IdpProxy.validateProtocol = function(protocol) {
|
||||
if (!protocol) {
|
||||
return; // falsy values turn into "default", so they are OK
|
||||
}
|
||||
let message = "Invalid protocol for identity provider; ";
|
||||
if (typeof protocol !== "string") {
|
||||
throw new Error(message + "must be a string");
|
||||
}
|
||||
if (decodeURIComponent(protocol).match(/[\/\\]/)) {
|
||||
throw new Error(message + "must not include '/' or '\\'");
|
||||
}
|
||||
};
|
||||
|
||||
IdpProxy.prototype = {
|
||||
_reset: function() {
|
||||
this.channel = null;
|
||||
this.ready = false;
|
||||
|
||||
this.counter = 0;
|
||||
this.tracking = {};
|
||||
this.pending = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a sandboxed iframe for hosting the idp-proxy's js. Create a message
|
||||
* channel down to the frame.
|
||||
*
|
||||
* @param errorCallback (function) a callback that will be invoked if there
|
||||
* is a fatal error starting the proxy
|
||||
*/
|
||||
start: function(errorCallback) {
|
||||
if (this.channel) {
|
||||
return;
|
||||
}
|
||||
let well_known = "https://" + this.domain;
|
||||
well_known += "/.well-known/idp-proxy/" + this.protocol;
|
||||
this.channel = new IdpChannel(well_known, this._messageReceived.bind(this));
|
||||
this.channel.open(function(error) {
|
||||
if (error) {
|
||||
this.close();
|
||||
if (typeof errorCallback === "function") {
|
||||
errorCallback(error);
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Send a message up to the idp proxy. This should be an RTC "SIGN" or
|
||||
* "VERIFY" message. This method adds the tracking 'id' parameter
|
||||
* automatically to the message so that the callback is only invoked for the
|
||||
* response to the message.
|
||||
*
|
||||
* The caller is responsible for ensuring that a response is received. If the
|
||||
* IdP doesn't respond, the callback simply isn't invoked.
|
||||
*/
|
||||
send: function(message, callback) {
|
||||
this.start();
|
||||
if (this.ready) {
|
||||
message.id = "" + (++this.counter);
|
||||
this.tracking[message.id] = callback;
|
||||
this.channel.send(message);
|
||||
} else {
|
||||
this.pending.push({ message: message, callback: callback });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a message from the IdP. This automatically sends if the message is
|
||||
* 'READY' so there is no need to track readiness state outside of this obj.
|
||||
*/
|
||||
_messageReceived: function(message) {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
if (message.type === "READY") {
|
||||
this.ready = true;
|
||||
this.pending.forEach(function(p) {
|
||||
this.send(p.message, p.callback);
|
||||
}, this);
|
||||
this.pending = [];
|
||||
} else if (this.tracking[message.id]) {
|
||||
var callback = this.tracking[message.id];
|
||||
delete this.tracking[message.id];
|
||||
callback(message);
|
||||
} else {
|
||||
let console = Cc["@mozilla.org/consoleservice;1"].
|
||||
getService(Ci.nsIConsoleService);
|
||||
console.logStringMessage("Received bad message from IdP: " +
|
||||
message.id + ":" + message.type);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Performs cleanup. The object should be OK to use again.
|
||||
*/
|
||||
close: function() {
|
||||
if (!this.channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// dump a message of type "ERROR" in response to all outstanding
|
||||
// messages to the IdP
|
||||
let error = { type: "ERROR" };
|
||||
Object.keys(this.tracking).forEach(function(k) {
|
||||
this.tracking[k](error);
|
||||
}, this);
|
||||
this.pending.forEach(function(p) {
|
||||
p.callback(error);
|
||||
}, this);
|
||||
|
||||
this.channel.close();
|
||||
this._reset();
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return this.domain + '/' + this.protocol;
|
||||
}
|
||||
};
|
||||
|
||||
this.IdpProxy = IdpProxy;
|
@ -1,3 +1,4 @@
|
||||
/* jshint moz:true, browser:true */
|
||||
/* 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/. */
|
||||
|
@ -40,6 +40,12 @@ EXTRA_COMPONENTS += [
|
||||
'PeerConnection.manifest',
|
||||
]
|
||||
|
||||
JS_MODULES_PATH = 'modules/media'
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'IdpProxy.jsm',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_B2G']:
|
||||
EXPORTS.mozilla += [
|
||||
'MediaPermissionGonk.h',
|
||||
|
@ -7,3 +7,7 @@ ifdef MOZ_WEBRTC_LEAKING_TESTS
|
||||
MOCHITEST_FILES += \
|
||||
$(NULL)
|
||||
endif
|
||||
|
||||
INSTALL_TARGETS += idpproxy
|
||||
idpproxy_FILES := idp.html idp-proxy.js
|
||||
idpproxy_DEST := $(DEPTH)/_tests/testing/mochitest/.well-known/idp-proxy/
|
||||
|
@ -117,7 +117,10 @@ function runTest(aCallback) {
|
||||
// Running as a Mochitest.
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SpecialPowers.pushPrefEnv({'set': [
|
||||
['dom.messageChannel.enabled', true],
|
||||
['media.peerconnection.enabled', true],
|
||||
['media.peerconnection.identity.enabled', true],
|
||||
['media.peerconnection.identity.timeout', 1000],
|
||||
['media.navigator.permission.disabled', true]]
|
||||
}, function () {
|
||||
try {
|
||||
|
96
dom/media/tests/mochitest/idp-proxy.js
Normal file
96
dom/media/tests/mochitest/idp-proxy.js
Normal file
@ -0,0 +1,96 @@
|
||||
(function(global) {
|
||||
"use strict";
|
||||
|
||||
function IDPJS() {
|
||||
this.domain = window.location.host;
|
||||
// so rather than create a million different IdP configurations and litter
|
||||
// the world with files all containing near-identical code, let's use the
|
||||
// hash/URL fragment as a way of generating instructions for the IdP
|
||||
this.instructions = window.location.hash.replace("#", "").split(":");
|
||||
this.port = window.rtcwebIdentityPort;
|
||||
this.port.onmessage = this.receiveMessage.bind(this);
|
||||
this.sendResponse({
|
||||
type : "READY"
|
||||
});
|
||||
}
|
||||
|
||||
IDPJS.prototype.getDelay = function() {
|
||||
// instructions in the form "delay123" have that many milliseconds
|
||||
// added before sending the response
|
||||
var delay = 0;
|
||||
function addDelay(instruction) {
|
||||
var m = instruction.match(/^delay(\d+)$/);
|
||||
if (m) {
|
||||
delay += parseInt(m[1], 10);
|
||||
}
|
||||
}
|
||||
this.instructions.forEach(addDelay);
|
||||
return delay;
|
||||
};
|
||||
|
||||
function is(target) {
|
||||
return function(instruction) {
|
||||
return instruction === target;
|
||||
};
|
||||
}
|
||||
|
||||
IDPJS.prototype.sendResponse = function(response) {
|
||||
// we don't touch the READY message unless told to
|
||||
if (response.type === "READY" && !this.instructions.some(is("ready"))) {
|
||||
this.port.postMessage(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// if any instruction is "error", return an error.
|
||||
if (this.instructions.some(is("error"))) {
|
||||
response.type = "ERROR";
|
||||
}
|
||||
|
||||
window.setTimeout(function() {
|
||||
this.port.postMessage(response);
|
||||
}.bind(this), this.getDelay());
|
||||
};
|
||||
|
||||
IDPJS.prototype.receiveMessage = function(ev) {
|
||||
var message = ev.data;
|
||||
switch (message.type) {
|
||||
case "SIGN":
|
||||
this.sendResponse({
|
||||
type : "SUCCESS",
|
||||
id : message.id,
|
||||
message : {
|
||||
idp : {
|
||||
domain : this.domain,
|
||||
protocol : "idp.html"
|
||||
},
|
||||
assertion : JSON.stringify({
|
||||
identity : "someone@" + this.domain,
|
||||
contents : message.message
|
||||
})
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "VERIFY":
|
||||
this.sendResponse({
|
||||
type : "SUCCESS",
|
||||
id : message.id,
|
||||
message : {
|
||||
identity : {
|
||||
name : "someone@" + this.domain,
|
||||
displayname : "Someone"
|
||||
},
|
||||
contents : JSON.parse(message.message).contents
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this.sendResponse({
|
||||
type : "ERROR",
|
||||
error : JSON.stringify(message)
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
global.idp = new IDPJS();
|
||||
}(this));
|
11
dom/media/tests/mochitest/idp.html
Normal file
11
dom/media/tests/mochitest/idp.html
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>IDP Proxy</title>
|
||||
<script src="idp-proxy.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
Test IDP Proxy
|
||||
</body>
|
||||
</html>
|
@ -52,3 +52,4 @@ skip-if = os == 'mac'
|
||||
[test_peerConnection_setRemoteOfferInHaveLocalOffer.html]
|
||||
[test_peerConnection_throwInCallbacks.html]
|
||||
[test_peerConnection_toJSON.html]
|
||||
[test_idpproxy.html]
|
||||
|
122
dom/media/tests/mochitest/test_idpproxy.html
Normal file
122
dom/media/tests/mochitest/test_idpproxy.html
Normal file
@ -0,0 +1,122 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script class="testbody" type="application/javascript">
|
||||
"use strict";
|
||||
var Cu = SpecialPowers.Cu;
|
||||
var rtcid = Cu.import("resource://gre/modules/media/IdpProxy.jsm");
|
||||
var IdpProxy = rtcid.IdpProxy;
|
||||
var request = {
|
||||
type: "SIGN",
|
||||
message: "foo"
|
||||
};
|
||||
|
||||
function test_domain_sandbox(done) {
|
||||
var diabolical = {
|
||||
toString : function() {
|
||||
return "example.com/path";
|
||||
}
|
||||
};
|
||||
var domains = [ "ex/foo", "user@ex", "user:pass@ex", "ex#foo", "ex?foo",
|
||||
"", 12, null, diabolical, true ];
|
||||
domains.forEach(function(domain) {
|
||||
try {
|
||||
var idp = new IdpProxy(domain);
|
||||
ok(false, "IdpProxy didn't catch bad domain: " + domain);
|
||||
} catch (e) {
|
||||
var str = (typeof domain === "string") ? domain : typeof domain;
|
||||
ok(true, "Evil domain '" + str + "' raises exception");
|
||||
}
|
||||
});
|
||||
done();
|
||||
}
|
||||
|
||||
function test_protocol_sandbox(done) {
|
||||
var protos = [ "../evil/proto", "..%2Fevil%2Fproto",
|
||||
"\\evil", "%5cevil", 12, true, {} ];
|
||||
protos.forEach(function(proto) {
|
||||
try {
|
||||
var idp = new IdpProxy("example.com", proto);
|
||||
ok(false, "IdpProxy didn't catch bad protocol: " + proto);
|
||||
} catch (e) {
|
||||
var str = (typeof proto === "string") ? proto : typeof proto;
|
||||
ok(true, "Evil protocol '" + proto + "' raises exception");
|
||||
}
|
||||
});
|
||||
done();
|
||||
}
|
||||
|
||||
function handleFailure(done) {
|
||||
return function failure(error) {
|
||||
ok(false, "IdP error" + error);
|
||||
done();
|
||||
};
|
||||
}
|
||||
|
||||
function test_success_response(done) {
|
||||
var idp;
|
||||
|
||||
function handleResponse(response) {
|
||||
is(SpecialPowers.wrap(response).type, "SUCCESS", "IdP responds with SUCCESS");
|
||||
idp.close();
|
||||
done();
|
||||
}
|
||||
|
||||
idp = new IdpProxy("example.com", "idp.html");
|
||||
idp.start(handleFailure(done));
|
||||
idp.send(request, handleResponse);
|
||||
}
|
||||
|
||||
function test_error_response(done) {
|
||||
var idp;
|
||||
|
||||
function handleResponse(response) {
|
||||
is(SpecialPowers.wrap(response).type, "ERROR", "IdP should produce ERROR");
|
||||
idp.close();
|
||||
done();
|
||||
}
|
||||
|
||||
idp = new IdpProxy("example.com", "idp.html#error");
|
||||
idp.start(handleFailure(done));
|
||||
idp.send(request, handleResponse);
|
||||
}
|
||||
|
||||
function test_delayed_response(done) {
|
||||
var idp;
|
||||
|
||||
function handleResponse(response) {
|
||||
is(SpecialPowers.wrap(response).type, "SUCCESS",
|
||||
"IdP should handle delayed response");
|
||||
idp.close();
|
||||
done();
|
||||
}
|
||||
|
||||
idp = new IdpProxy("example.com", "idp.html#delay100");
|
||||
idp.start(handleFailure(done));
|
||||
idp.send(request, handleResponse);
|
||||
}
|
||||
|
||||
var TESTS = [ test_domain_sandbox, test_protocol_sandbox,
|
||||
test_success_response, test_error_response,
|
||||
test_delayed_response ];
|
||||
|
||||
function run_next_test() {
|
||||
if (TESTS.length) {
|
||||
var test = TESTS.shift();
|
||||
test(run_next_test);
|
||||
} else {
|
||||
SimpleTest.finish();
|
||||
}
|
||||
}
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SpecialPowers.pushPrefEnv({
|
||||
"set" : [ [ "dom.messageChannel.enabled", true ] ]
|
||||
}, run_next_test);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,6 +0,0 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
@ -4,6 +4,6 @@
|
||||
# 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/.
|
||||
|
||||
DIRS += ['chrome', 'mochitest']
|
||||
DIRS += ['chrome']
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
|
||||
|
Loading…
Reference in New Issue
Block a user