mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-11 21:58:41 +02:00
248 lines
8.3 KiB
JavaScript
248 lines
8.3 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
/**
|
|
* This is a mock server that implements the FxA endpoints on the Loop server.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri", "state"];
|
|
const HAWK_TOKEN_LENGTH = 64;
|
|
|
|
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
|
Components.utils.importGlobalProperties(["URL"]);
|
|
|
|
/**
|
|
* Entry point for HTTP requests.
|
|
*/
|
|
function handleRequest(request, response) {
|
|
// Convert the query string to a path with a placeholder base of example.com
|
|
let url = new URL(request.queryString.replace(/%3F.*/,""), "http://www.example.com");
|
|
dump("loop_fxa.sjs request for: " + url.pathname + "\n");
|
|
switch (url.pathname) {
|
|
case "/setup_params": // Test-only
|
|
setup_params(request, response);
|
|
return;
|
|
case "/fxa-oauth/params":
|
|
params(request, response);
|
|
return;
|
|
case "/" + encodeURIComponent("/oauth/authorization"):
|
|
oauth_authorization(request, response);
|
|
return;
|
|
case "/fxa-oauth/token":
|
|
token(request, response);
|
|
return;
|
|
case "/registration":
|
|
if (request.method == "DELETE") {
|
|
delete_registration(request, response);
|
|
} else {
|
|
registration(request, response);
|
|
}
|
|
return;
|
|
case "/get_registration": // Test-only
|
|
get_registration(request, response);
|
|
return;
|
|
case "/profile/profile":
|
|
profile(request, response);
|
|
return;
|
|
}
|
|
response.setStatusLine(request.httpVersion, 404, "Not Found");
|
|
}
|
|
|
|
/**
|
|
* POST /setup_params
|
|
* DELETE /setup_params
|
|
*
|
|
* Test-only endpoint to setup the /fxa-oauth/params response.
|
|
*
|
|
* For a POST the X-Params header should contain a JSON object with keys to set for /fxa-oauth/params.
|
|
* A DELETE request will delete the stored parameters and should be run in a cleanup function to
|
|
* avoid interfering with subsequent tests.
|
|
*/
|
|
function setup_params(request, response) {
|
|
response.setHeader("Content-Type", "text/plain", false);
|
|
if (request.method == "DELETE") {
|
|
setSharedState("/fxa-oauth/params", "");
|
|
setSharedState("/registration", "");
|
|
response.write("Params deleted");
|
|
return;
|
|
}
|
|
let params = JSON.parse(request.getHeader("X-Params"));
|
|
if (!params) {
|
|
response.setStatusLine(request.httpVersion, 400, "Bad Request");
|
|
return;
|
|
}
|
|
setSharedState("/fxa-oauth/params", JSON.stringify(params));
|
|
response.write("Params updated");
|
|
}
|
|
|
|
/**
|
|
* POST /fxa-oauth/params endpoint
|
|
*
|
|
* Fetch OAuth parameters used to start the OAuth flow in the browser.
|
|
* Parameters: None
|
|
* Response: JSON containing an object of oauth parameters.
|
|
*/
|
|
function params(request, response) {
|
|
if (request.method != "POST") {
|
|
response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
|
|
response.setHeader("Allow", "POST", false);
|
|
|
|
// Add a button to make a POST request to make this endpoint easier to debug in the browser.
|
|
response.write("<form method=POST><button type=submit>POST</button></form>");
|
|
return;
|
|
}
|
|
|
|
let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
|
|
|
|
if (params.test_error && params.test_error == "params_401") {
|
|
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
|
|
response.write("401 Unauthorized");
|
|
return;
|
|
}
|
|
|
|
// Warn if required parameters are missing.
|
|
for (let paramName of REQUIRED_PARAMS) {
|
|
if (!(paramName in params)) {
|
|
dump("Warning: " + paramName + " is a required parameter\n");
|
|
}
|
|
}
|
|
|
|
// Save the result so we have the effective `state` value.
|
|
setSharedState("/fxa-oauth/params", JSON.stringify(params));
|
|
response.setHeader("Content-Type", "application/json; charset=utf-8", false);
|
|
|
|
let client_id = params.client_id || "";
|
|
// Pad the client_id with "X" until the token length to simulate a token
|
|
let padding = "X".repeat(HAWK_TOKEN_LENGTH - client_id.length);
|
|
if (params.test_error !== "params_no_hawk") {
|
|
response.setHeader("Hawk-Session-Token", client_id + padding, false);
|
|
}
|
|
|
|
response.write(JSON.stringify(params, null, 2));
|
|
}
|
|
|
|
/**
|
|
* GET /oauth/authorization endpoint for the test params.
|
|
*
|
|
* Redirect to a test page that uses WebChannel to complete the web flow.
|
|
*/
|
|
function oauth_authorization(request, response) {
|
|
response.setStatusLine(request.httpVersion, 302, "Found");
|
|
response.setHeader("Location", "browser_fxa_oauth.html");
|
|
}
|
|
|
|
/**
|
|
* POST /fxa-oauth/token
|
|
*
|
|
* Validate the state parameter with the server session state and if it matches, exchange the code
|
|
* for an OAuth Token.
|
|
* Parameters: code & state as JSON in the POST body.
|
|
* Response: JSON containing an object of OAuth token information.
|
|
*/
|
|
function token(request, response) {
|
|
let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
|
|
|
|
if (params.test_error && params.test_error == "token_401") {
|
|
response.setStatusLine(request.httpVersion, 401, "Unauthorized");
|
|
response.write("401 Unauthorized");
|
|
return;
|
|
}
|
|
|
|
if (!request.hasHeader("Authorization") ||
|
|
!request.getHeader("Authorization").startsWith("Hawk")) {
|
|
response.setStatusLine(request.httpVersion, 401, "Missing Hawk");
|
|
response.write("401 Missing Hawk Authorization header");
|
|
return;
|
|
}
|
|
|
|
let body = NetUtil.readInputStreamToString(request.bodyInputStream,
|
|
request.bodyInputStream.available());
|
|
let payload = JSON.parse(body);
|
|
if (!params.state || params.state !== payload.state) {
|
|
response.setStatusLine(request.httpVersion, 400, "State mismatch");
|
|
response.write("State mismatch");
|
|
return;
|
|
}
|
|
|
|
let tokenData = {
|
|
access_token: payload.code + "_access_token",
|
|
scope: "profile",
|
|
token_type: "bearer",
|
|
};
|
|
response.setHeader("Content-Type", "application/json; charset=utf-8", false);
|
|
response.write(JSON.stringify(tokenData, null, 2));
|
|
}
|
|
|
|
/**
|
|
* GET /profile
|
|
*
|
|
*/
|
|
function profile(request, response) {
|
|
response.setHeader("Content-Type", "application/json; charset=utf-8", false);
|
|
let profile = {
|
|
email: "test@example.com",
|
|
uid: "1234abcd",
|
|
};
|
|
response.write(JSON.stringify(profile, null, 2));
|
|
}
|
|
|
|
/**
|
|
* POST /registration
|
|
*
|
|
* Mock Loop registration endpoint. Hawk Authorization headers are expected only for FxA sessions.
|
|
*/
|
|
function registration(request, response) {
|
|
let body = NetUtil.readInputStreamToString(request.bodyInputStream,
|
|
request.bodyInputStream.available());
|
|
let payload = JSON.parse(body);
|
|
if ((payload.simplePushURL == "https://localhost/pushUrl/fxa" ||
|
|
payload.simplePushURLs.calls == "https://localhost/pushUrl/fxa-calls" ||
|
|
payload.simplePushURLs.rooms == "https://localhost/pushUrl/fxa-rooms") &&
|
|
(!request.hasHeader("Authorization") ||
|
|
!request.getHeader("Authorization").startsWith("Hawk"))) {
|
|
response.setStatusLine(request.httpVersion, 401, "Missing Hawk");
|
|
response.write("401 Missing Hawk Authorization header");
|
|
return;
|
|
}
|
|
setSharedState("/registration", body);
|
|
}
|
|
|
|
/**
|
|
* DELETE /registration
|
|
*
|
|
* Hawk Authorization headers are required.
|
|
*/
|
|
function delete_registration(request, response) {
|
|
if (!request.hasHeader("Authorization") ||
|
|
!request.getHeader("Authorization").startsWith("Hawk")) {
|
|
response.setStatusLine(request.httpVersion, 401, "Missing Hawk");
|
|
response.write("401 Missing Hawk Authorization header");
|
|
return;
|
|
}
|
|
|
|
// Do some query string munging due to the SJS file using a base with a trailing "?"
|
|
// making the path become a query parameter. This is because we aren't actually
|
|
// registering endpoints at the root of the hostname e.g. /registration.
|
|
let url = new URL(request.queryString.replace(/%3F.*/,""), "http://www.example.com");
|
|
let state = getSharedState("/registration");
|
|
if (state != "") { //Already set to empty value on a successful channel unregsitration.
|
|
let registration = JSON.parse(state);
|
|
if (registration.simplePushURLs.calls == url.searchParams.get("simplePushURL")) {
|
|
setSharedState("/registration", "");
|
|
} else {
|
|
response.setStatusLine(request.httpVersion, 400, "Bad Request");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /get_registration
|
|
*
|
|
* Used for testing purposes to check if registration succeeded by returning the POST body.
|
|
*/
|
|
function get_registration(request, response) {
|
|
response.setHeader("Content-Type", "application/json; charset=utf-8", false);
|
|
response.write(getSharedState("/registration"));
|
|
}
|