browser/components/loop/test/mochitest/loop_fxa.sjs
author Andrea Marchesini <amarchesini@mozilla.com>
Thu, 23 Oct 2014 12:48:44 +0100
changeset 211912 2340c1d21e94d56517970e03ad993d692d2a5af2
parent 206147 0519bf931920f62e037fbed4798d2173bf348bae
child 211935 c9d867500a397548fc8890ea8a7185b2338f2ce5
permissions -rw-r--r--
Bug 1082734 - Remove URL.searchParams to avoid the stealing of data from future cross-origin sites, r=bz

/* 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", "URLSearchParams"]);

/**
 * 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" &&
       (!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 registration = JSON.parse(getSharedState("/registration"));
  let searchParams = new URLSearchParams(url.search.substr(1));
  if (registration.simplePushURL == 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"));
}