tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_net_copy_as_curl.js (7348B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 /**
      7 * Tests if Copy as cURL works.
      8 */
      9 
     10 const POST_PAYLOAD = "Plaintext value as a payload";
     11 
     12 add_task(async function () {
     13  const { tab, monitor } = await initNetMonitor(HTTPS_CURL_URL, {
     14    requestCount: 1,
     15  });
     16  // disable sending idempotency header for POST request
     17  await pushPref("network.http.idempotencyKey.enabled", false);
     18  info("Starting test... ");
     19 
     20  // Different quote chars are used for Windows and POSIX
     21  const QUOTE_WIN = '^"';
     22  const QUOTE_POSIX = "'";
     23 
     24  const isWin = Services.appinfo.OS === "WINNT";
     25  const testData = isWin
     26    ? [
     27        {
     28          menuItemId: "request-list-context-copy-as-curl-win",
     29          data: buildTestData(QUOTE_WIN, true),
     30        },
     31        {
     32          menuItemId: "request-list-context-copy-as-curl-posix",
     33          data: buildTestData(QUOTE_POSIX, false),
     34        },
     35      ]
     36    : [
     37        {
     38          menuItemId: "request-list-context-copy-as-curl",
     39          data: buildTestData(QUOTE_POSIX, false),
     40        },
     41      ];
     42 
     43  await testForPlatform(tab, monitor, testData);
     44 
     45  await teardown(monitor);
     46 });
     47 
     48 function buildTestData(QUOTE, isWin) {
     49  // Quote a string, escape the quotes inside the string
     50  function quote(str) {
     51    return QUOTE + str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`) + QUOTE;
     52  }
     53 
     54  // Header param is formatted as -H "Header: value" or -H 'Header: value'
     55  function header(h) {
     56    return "-H " + quote(h);
     57  }
     58 
     59  const CMD = isWin ? "curl.exe " : "curl ";
     60 
     61  // Construct the expected command
     62  const SIMPLE_BASE = [CMD + quote(HTTPS_SIMPLE_SJS)];
     63  const SLOW_BASE = [CMD + quote(HTTPS_SLOW_SJS)];
     64  const BASE_RESULT = [
     65    "--compressed",
     66    header("User-Agent: " + navigator.userAgent),
     67    header("Accept: */*"),
     68    header("Accept-Language: " + navigator.language),
     69    header("X-Custom-Header-1: Custom value"),
     70    header("X-Custom-Header-2: 8.8.8.8"),
     71    header("X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"),
     72    header("Referer: " + HTTPS_CURL_URL),
     73    header("Connection: keep-alive"),
     74    header("Pragma: no-cache"),
     75    header("Cache-Control: no-cache"),
     76    header("Sec-Fetch-Dest: empty"),
     77    header("Sec-Fetch-Mode: cors"),
     78    header("Sec-Fetch-Site: same-origin"),
     79  ];
     80 
     81  const COOKIE_PARTIAL_RESULT = [header("Cookie: bob=true; tom=cool")];
     82 
     83  const POST_PARTIAL_RESULT = [
     84    "-X",
     85    "POST",
     86    "--data-raw " + quote(POST_PAYLOAD),
     87    header("Content-Type: text/plain;charset=UTF-8"),
     88  ];
     89  const ORIGIN_RESULT = [header("Origin: https://example.com")];
     90 
     91  const HEAD_PARTIAL_RESULT = ["-I"];
     92 
     93  return {
     94    SIMPLE_BASE,
     95    SLOW_BASE,
     96    BASE_RESULT,
     97    COOKIE_PARTIAL_RESULT,
     98    POST_PAYLOAD,
     99    POST_PARTIAL_RESULT,
    100    ORIGIN_RESULT,
    101    HEAD_PARTIAL_RESULT,
    102  };
    103 }
    104 
    105 async function testForPlatform(tab, monitor, testData) {
    106  // GET request, no cookies (first request)
    107  await performRequest("GET");
    108  for (const test of testData) {
    109    await testClipboardContent(test.menuItemId, [
    110      ...test.data.SIMPLE_BASE,
    111      ...test.data.BASE_RESULT,
    112    ]);
    113  }
    114  // Check to make sure it is still OK after we view the response (bug#1452442)
    115  await selectIndexAndWaitForSourceEditor(monitor, 0);
    116  for (const test of testData) {
    117    await testClipboardContent(test.menuItemId, [
    118      ...test.data.SIMPLE_BASE,
    119      ...test.data.BASE_RESULT,
    120    ]);
    121  }
    122 
    123  // GET request, cookies set by previous response
    124  await performRequest("GET");
    125  for (const test of testData) {
    126    await testClipboardContent(test.menuItemId, [
    127      ...test.data.SIMPLE_BASE,
    128      ...test.data.BASE_RESULT,
    129      ...test.data.COOKIE_PARTIAL_RESULT,
    130    ]);
    131  }
    132 
    133  // Unfinished request (bug#1378464, bug#1420513)
    134  const waitSlow = waitForNetworkEvents(monitor, 0);
    135  await SpecialPowers.spawn(
    136    tab.linkedBrowser,
    137    [HTTPS_SLOW_SJS],
    138    async function (url) {
    139      content.wrappedJSObject.performRequest(url, "GET", null);
    140    }
    141  );
    142  await waitSlow;
    143  for (const test of testData) {
    144    await testClipboardContent(test.menuItemId, [
    145      ...test.data.SLOW_BASE,
    146      ...test.data.BASE_RESULT,
    147      ...test.data.COOKIE_PARTIAL_RESULT,
    148    ]);
    149  }
    150 
    151  // POST request
    152  await performRequest("POST", POST_PAYLOAD);
    153  for (const test of testData) {
    154    await testClipboardContent(test.menuItemId, [
    155      ...test.data.SIMPLE_BASE,
    156      ...test.data.BASE_RESULT,
    157      ...test.data.COOKIE_PARTIAL_RESULT,
    158      ...test.data.POST_PARTIAL_RESULT,
    159      ...test.data.ORIGIN_RESULT,
    160    ]);
    161  }
    162 
    163  // HEAD request
    164  await performRequest("HEAD");
    165  for (const test of testData) {
    166    await testClipboardContent(test.menuItemId, [
    167      ...test.data.SIMPLE_BASE,
    168      ...test.data.BASE_RESULT,
    169      ...test.data.COOKIE_PARTIAL_RESULT,
    170      ...test.data.HEAD_PARTIAL_RESULT,
    171    ]);
    172  }
    173 
    174  async function performRequest(method, payload) {
    175    const waitRequest = waitForNetworkEvents(monitor, 1);
    176    await SpecialPowers.spawn(
    177      tab.linkedBrowser,
    178      [
    179        {
    180          url: HTTPS_SIMPLE_SJS,
    181          method_: method,
    182          payload_: payload,
    183        },
    184      ],
    185      async function ({ url, method_, payload_ }) {
    186        content.wrappedJSObject.performRequest(url, method_, payload_);
    187      }
    188    );
    189    await waitRequest;
    190  }
    191 
    192  async function testClipboardContent(menuItemId, expectedResult) {
    193    const { document } = monitor.panelWin;
    194 
    195    const items = document.querySelectorAll(".request-list-item");
    196    const itemIndex = items.length - 1;
    197    EventUtils.sendMouseEvent({ type: "mousedown" }, items[itemIndex]);
    198    EventUtils.sendMouseEvent(
    199      { type: "contextmenu" },
    200      document.querySelectorAll(".request-list-item")[0]
    201    );
    202 
    203    /* Ensure that the copy as cURL option is always visible */
    204    is(
    205      !!getContextMenuItem(monitor, menuItemId),
    206      true,
    207      `The "Copy as cURL" context menu item "${menuItemId}" should not be hidden.`
    208    );
    209 
    210    await waitForClipboardPromise(
    211      async function setup() {
    212        await selectContextMenuItem(monitor, menuItemId);
    213      },
    214      function validate(result) {
    215        if (typeof result !== "string") {
    216          return false;
    217        }
    218 
    219        // Different setups may produce the same command, but with the
    220        // parameters in a different order in the commandline (which is fine).
    221        // Here we confirm that the commands are the same even in that case.
    222 
    223        // This monster regexp parses the command line into an array of arguments,
    224        // recognizing quoted args with matching quotes and escaped quotes inside:
    225        // [ "curl.exe 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
    226        // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
    227        const matchRe = /[-\.A-Za-z1-9]+(?: ([\^\"']+)(?:\\\1|.)*?\1)?/g;
    228 
    229        const actual = result.match(matchRe);
    230        // Must begin with the same "curl 'URL'" segment
    231        if (!actual || expectedResult[0] != actual[0]) {
    232          return false;
    233        }
    234 
    235        // Must match each of the params in the middle (headers)
    236        return (
    237          expectedResult.length === actual.length &&
    238          expectedResult.some(param => actual.includes(param))
    239        );
    240      }
    241    );
    242 
    243    info(
    244      `Clipboard contains a cURL command for item ${itemIndex} by "${menuItemId}"`
    245    );
    246  }
    247 }