mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 727210 - Implement client for Services' token server; r=rnewman
This commit is contained in:
parent
889eca9a49
commit
5d37ab4a4d
@ -18,6 +18,7 @@ modules := \
|
||||
preferences.js \
|
||||
rest.js \
|
||||
stringbundle.js \
|
||||
tokenserverclient.js \
|
||||
utils.js \
|
||||
$(NULL)
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
const EXPORTED_SYMBOLS = ["RESTRequest"];
|
||||
const EXPORTED_SYMBOLS = ["RESTRequest", "RESTResponse"];
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
@ -3,3 +3,5 @@
|
||||
|
||||
pref("services.common.log.logger.rest.request", "Debug");
|
||||
pref("services.common.log.logger.rest.response", "Debug");
|
||||
|
||||
pref("services.common.tokenserverclient.logger.level", "Info");
|
||||
|
@ -7,6 +7,7 @@ const modules = [
|
||||
"preferences.js",
|
||||
"rest.js",
|
||||
"stringbundle.js",
|
||||
"tokenserverclient.js",
|
||||
"utils.js",
|
||||
];
|
||||
|
||||
|
166
services/common/tests/unit/test_tokenserverclient.js
Normal file
166
services/common/tests/unit/test_tokenserverclient.js
Normal file
@ -0,0 +1,166 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://services-common/async.js");
|
||||
Cu.import("resource://services-common/tokenserverclient.js");
|
||||
|
||||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_test(function test_working_bid_exchange() {
|
||||
_("Ensure that working BrowserID token exchange works as expected.");
|
||||
|
||||
let service = "http://example.com/foo";
|
||||
|
||||
let server = httpd_setup({
|
||||
"/1.0/foo/1.0": function(request, response) {
|
||||
do_check_true(request.hasHeader("accept"));
|
||||
do_check_eq("application/json", request.getHeader("accept"));
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
|
||||
let body = JSON.stringify({
|
||||
id: "id",
|
||||
secret: "key",
|
||||
api_endpoint: service,
|
||||
uid: "uid",
|
||||
});
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new TokenServerClient();
|
||||
let cb = Async.makeSpinningCallback();
|
||||
let url = TEST_SERVER_URL + "1.0/foo/1.0";
|
||||
client.getTokenFromBrowserIDAssertion(url, "assertion", cb);
|
||||
let result = cb.wait();
|
||||
do_check_eq("object", typeof(result));
|
||||
do_check_attribute_count(result, 4);
|
||||
do_check_eq(service, result.endpoint);
|
||||
do_check_eq("id", result.id);
|
||||
do_check_eq("key", result.key);
|
||||
do_check_eq("uid", result.uid);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
|
||||
add_test(function test_invalid_arguments() {
|
||||
_("Ensure invalid arguments to APIs are rejected.");
|
||||
|
||||
let args = [
|
||||
[null, "assertion", function() {}],
|
||||
["http://example.com/", null, function() {}],
|
||||
["http://example.com/", "assertion", null]
|
||||
];
|
||||
|
||||
for each (let arg in args) {
|
||||
try {
|
||||
let client = new TokenServerClient();
|
||||
client.getTokenFromBrowserIDAssertion(arg[0], arg[1], arg[2]);
|
||||
do_throw("Should never get here.");
|
||||
} catch (ex) {
|
||||
do_check_true(ex instanceof TokenServerClientError);
|
||||
}
|
||||
}
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_error_404() {
|
||||
_("Ensure that 404 responses result in error.");
|
||||
|
||||
let server = httpd_setup();
|
||||
|
||||
let client = new TokenServerClient();
|
||||
let url = TEST_SERVER_URL + "foo";
|
||||
client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
|
||||
do_check_neq(null, error);
|
||||
do_check_eq("TokenServerClientServerError", error.name);
|
||||
do_check_neq(null, error.response);
|
||||
do_check_eq(null, r);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_bad_json() {
|
||||
_("Ensure that malformed JSON is handled properly.");
|
||||
|
||||
let server = httpd_setup({
|
||||
"/1.0/foo/1.0": function(request, response) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
|
||||
let body = '{"id": "id", baz}'
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
});
|
||||
|
||||
let client = new TokenServerClient();
|
||||
let url = TEST_SERVER_URL + "1.0/foo/1.0";
|
||||
client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
|
||||
_(error);
|
||||
do_check_neq(null, error);
|
||||
do_check_eq("TokenServerClientServerError", error.name);
|
||||
do_check_neq(null, error.response);
|
||||
do_check_eq(null, r);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_unhandled_media_type() {
|
||||
_("Ensure that unhandled media types throw an error.");
|
||||
|
||||
let server = httpd_setup({
|
||||
"/1.0/foo/1.0": function(request, response) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "text/plain");
|
||||
|
||||
let body = "hello, world";
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
});
|
||||
|
||||
let url = TEST_SERVER_URL + "1.0/foo/1.0";
|
||||
let client = new TokenServerClient();
|
||||
client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
|
||||
do_check_neq(null, error);
|
||||
do_check_eq("TokenServerClientError", error.name);
|
||||
do_check_neq(null, error.response);
|
||||
do_check_eq(null, r);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_rich_media_types() {
|
||||
_("Ensure that extra tokens in the media type aren't rejected.");
|
||||
|
||||
let server = httpd_setup({
|
||||
"/foo": function(request, response) {
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("Content-Type", "application/json; foo=bar; bar=foo");
|
||||
|
||||
let body = JSON.stringify({
|
||||
id: "id",
|
||||
secret: "key",
|
||||
api_endpoint: "foo",
|
||||
uid: "uid",
|
||||
});
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
}
|
||||
});
|
||||
|
||||
let url = TEST_SERVER_URL + "foo";
|
||||
let client = new TokenServerClient();
|
||||
client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
|
||||
do_check_eq(null, error);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
@ -17,3 +17,4 @@ tail =
|
||||
[test_observers.js]
|
||||
[test_preferences.js]
|
||||
[test_restrequest.js]
|
||||
[test_tokenserverclient.js]
|
||||
|
238
services/common/tokenserverclient.js
Normal file
238
services/common/tokenserverclient.js
Normal file
@ -0,0 +1,238 @@
|
||||
/* 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 = [
|
||||
"TokenServerClient",
|
||||
"TokenServerClientError",
|
||||
"TokenServerClientNetworkError",
|
||||
"TokenServerClientServerError"
|
||||
];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
|
||||
const Prefs = new Preferences("services.common.tokenserverclient.");
|
||||
|
||||
/**
|
||||
* Represents a TokenServerClient error that occurred on the client.
|
||||
*
|
||||
* This is the base type for all errors raised by client operations.
|
||||
*
|
||||
* @param message
|
||||
* (string) Error message.
|
||||
*/
|
||||
function TokenServerClientError(message) {
|
||||
this.name = "TokenServerClientError";
|
||||
this.message = message || "Client error.";
|
||||
}
|
||||
TokenServerClientError.prototype = new Error();
|
||||
TokenServerClientError.prototype.constructor = TokenServerClientError;
|
||||
|
||||
/**
|
||||
* Represents a TokenServerClient error that occurred in the network layer.
|
||||
*
|
||||
* @param error
|
||||
* The underlying error thrown by the network layer.
|
||||
*/
|
||||
function TokenServerClientNetworkError(error) {
|
||||
this.name = "TokenServerClientNetworkError";
|
||||
this.error = error;
|
||||
}
|
||||
TokenServerClientNetworkError.prototype = new TokenServerClientError();
|
||||
TokenServerClientNetworkError.prototype.constructor =
|
||||
TokenServerClientNetworkError;
|
||||
|
||||
/**
|
||||
* Represents a TokenServerClient error that occurred on the server.
|
||||
*
|
||||
* This type will be encountered for all non-200 response codes from the
|
||||
* server.
|
||||
*
|
||||
* @param message
|
||||
* (string) Error message.
|
||||
*/
|
||||
function TokenServerClientServerError(message) {
|
||||
this.name = "TokenServerClientServerError";
|
||||
this.message = message || "Server error.";
|
||||
}
|
||||
TokenServerClientServerError.prototype = new TokenServerClientError();
|
||||
TokenServerClientServerError.prototype.constructor =
|
||||
TokenServerClientServerError;
|
||||
|
||||
/**
|
||||
* Represents a client to the Token Server.
|
||||
*
|
||||
* http://docs.services.mozilla.com/token/index.html
|
||||
*
|
||||
* The Token Server supports obtaining tokens for arbitrary apps by
|
||||
* constructing URI paths of the form <app>/<app_version>. However, the service
|
||||
* discovery mechanism emphasizes the use of full URIs and tries to not force
|
||||
* the client to manipulate URIs. This client currently enforces this practice
|
||||
* by not implementing an API which would perform URI manipulation.
|
||||
*
|
||||
* If you are tempted to implement this API in the future, consider this your
|
||||
* warning that you may be doing it wrong and that you should store full URIs
|
||||
* instead.
|
||||
*
|
||||
* Areas to Improve:
|
||||
*
|
||||
* - The server sends a JSON response on error. The client does not currently
|
||||
* parse this. It might be convenient if it did.
|
||||
* - Currently all non-200 status codes are rolled into one error type. It
|
||||
* might be helpful if callers had a richer API that communicated who was
|
||||
* at fault (e.g. differentiating a 503 from a 401).
|
||||
*/
|
||||
function TokenServerClient() {
|
||||
this._log = Log4Moz.repository.getLogger("Common.TokenServerClient");
|
||||
this._log.level = Log4Moz.Level[Prefs.get("logger.level")];
|
||||
}
|
||||
TokenServerClient.prototype = {
|
||||
/**
|
||||
* Logger instance.
|
||||
*/
|
||||
_log: null,
|
||||
|
||||
/**
|
||||
* Obtain a token from a BrowserID assertion against a specific URL.
|
||||
*
|
||||
* This asynchronously obtains the token. The callback receives 2 arguments.
|
||||
* The first signifies an error and is a TokenServerClientError (or derived)
|
||||
* type when an error occurs. If an HTTP response was seen, a RESTResponse
|
||||
* instance will be stored in the "response" property of this object.
|
||||
*
|
||||
* The second argument to the callback is a map containing the results from
|
||||
* the server. This map has the following keys:
|
||||
*
|
||||
* id (string) HTTP MAC public key identifier.
|
||||
* key (string) HTTP MAC shared symmetric key.
|
||||
* endpoint (string) URL where service can be connected to.
|
||||
* uid (string) user ID for requested service.
|
||||
*
|
||||
* e.g.
|
||||
*
|
||||
* let client = new TokenServerClient();
|
||||
* let assertion = getBrowserIDAssertionFromSomewhere();
|
||||
* let url = "https://token.services.mozilla.com/1.0/sync/2.0";
|
||||
*
|
||||
* client.getTokenFromBrowserIDAssertion(url, assertion,
|
||||
* function(error, result) {
|
||||
* if (error) {
|
||||
* // Do error handling.
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* let {id: id, key: key, uid: uid, endpoint: endpoint} = result;
|
||||
* // Do stuff with data and carry on.
|
||||
* });
|
||||
*
|
||||
* @param url
|
||||
* (string) URL to fetch token from.
|
||||
* @param assertion
|
||||
* (string) BrowserID assertion to exchange token for.
|
||||
* @param cb
|
||||
* (function) Callback to be invoked with result of operation.
|
||||
*/
|
||||
getTokenFromBrowserIDAssertion:
|
||||
function getTokenFromBrowserIDAssertion(url, assertion, cb) {
|
||||
if (!url) {
|
||||
throw new TokenServerClientError("url argument is not valid.");
|
||||
}
|
||||
|
||||
if (!assertion) {
|
||||
throw new TokenServerClientError("assertion argument is not valid.");
|
||||
}
|
||||
|
||||
if (!cb) {
|
||||
throw new TokenServerClientError("cb argument is not valid.");
|
||||
}
|
||||
|
||||
this._log.debug("Beginning BID assertion exchange: " + url);
|
||||
|
||||
let req = new RESTRequest(url);
|
||||
req.setHeader("accept", "application/json");
|
||||
req.setHeader("authorization", "Browser-ID " + assertion);
|
||||
let client = this;
|
||||
req.get(function onResponse(error) {
|
||||
if (error) {
|
||||
cb(new TokenServerClientNetworkError(error), null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
client._processTokenResponse(this.response, cb);
|
||||
} catch (ex) {
|
||||
let error = new TokenServerClientError(ex);
|
||||
error.response = this.response;
|
||||
cb(error, null);
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler to process token request responses.
|
||||
*
|
||||
* @param response
|
||||
* RESTResponse from token HTTP request.
|
||||
* @param cb
|
||||
* The original callback passed to the public API.
|
||||
*/
|
||||
_processTokenResponse: function processTokenResponse(response, cb) {
|
||||
this._log.debug("Got token response.");
|
||||
|
||||
if (!response.success) {
|
||||
this._log.info("Non-200 response code to token request: " +
|
||||
response.status);
|
||||
this._log.debug("Response body: " + response.body);
|
||||
let error = new TokenServerClientServerError("Non 200 response code: " +
|
||||
response.status);
|
||||
error.response = response;
|
||||
cb(error, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let ct = response.headers["content-type"];
|
||||
if (ct != "application/json" && ct.indexOf("application/json;") != 0) {
|
||||
let error = new TokenServerClientError("Unsupported media type: " + ct);
|
||||
error.response = response;
|
||||
cb(error, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(response.body);
|
||||
} catch (ex) {
|
||||
let error = new TokenServerClientServerError("Invalid JSON returned " +
|
||||
"from server.");
|
||||
error.response = response;
|
||||
cb(error, null);
|
||||
return;
|
||||
}
|
||||
|
||||
for each (let k in ["id", "secret", "api_endpoint", "uid"]) {
|
||||
if (!(k in result)) {
|
||||
let error = new TokenServerClientServerError("Expected key not " +
|
||||
" present in result: " +
|
||||
k);
|
||||
error.response = response;
|
||||
cb(error, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._log.debug("Successful token response: " + result.id);
|
||||
cb(null, {
|
||||
id: result.id,
|
||||
key: result.secret,
|
||||
endpoint: result.api_endpoint,
|
||||
uid: result.uid,
|
||||
});
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user