tor-browser

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

browser_net_curl-utils.js (11742B)


      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 Curl Utils functionality.
      8 */
      9 
     10 const {
     11  Curl,
     12  CurlUtils,
     13 } = require("resource://devtools/client/shared/curl.js");
     14 
     15 add_task(async function () {
     16  const { tab, monitor } = await initNetMonitor(HTTPS_CURL_UTILS_URL, {
     17    requestCount: 1,
     18  });
     19  info("Starting test... ");
     20 
     21  const { store, windowRequire, connector } = monitor.panelWin;
     22  const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
     23  const { getSortedRequests } = windowRequire(
     24    "devtools/client/netmonitor/src/selectors/index"
     25  );
     26  const { getLongString, requestData } = connector;
     27 
     28  store.dispatch(Actions.batchEnable(false));
     29 
     30  const wait = waitForNetworkEvents(monitor, 6);
     31  await SpecialPowers.spawn(
     32    tab.linkedBrowser,
     33    [HTTPS_SIMPLE_SJS],
     34    async function (url) {
     35      content.wrappedJSObject.performRequests(url);
     36    }
     37  );
     38  await wait;
     39 
     40  const requests = {
     41    get: getSortedRequests(store.getState())[0],
     42    post: getSortedRequests(store.getState())[1],
     43    postJson: getSortedRequests(store.getState())[2],
     44    patch: getSortedRequests(store.getState())[3],
     45    multipart: getSortedRequests(store.getState())[4],
     46    multipartForm: getSortedRequests(store.getState())[5],
     47  };
     48 
     49  let data = await createCurlData(requests.get, getLongString, requestData);
     50  testFindHeader(data);
     51 
     52  data = await createCurlData(requests.post, getLongString, requestData);
     53  testIsUrlEncodedRequest(data);
     54  testWritePostDataTextParams(data);
     55  testWriteEmptyPostDataTextParams(data);
     56  testDataArgumentOnGeneratedCommand(data);
     57 
     58  data = await createCurlData(requests.patch, getLongString, requestData);
     59  testWritePostDataTextParams(data);
     60  testDataArgumentOnGeneratedCommand(data);
     61 
     62  data = await createCurlData(requests.postJson, getLongString, requestData);
     63  testDataEscapeOnGeneratedCommand(data);
     64 
     65  data = await createCurlData(requests.multipart, getLongString, requestData);
     66  testIsMultipartRequest(data);
     67  testGetMultipartBoundary(data);
     68  testMultiPartHeaders(data);
     69  testRemoveBinaryDataFromMultipartText(data);
     70 
     71  data = await createCurlData(
     72    requests.multipartForm,
     73    getLongString,
     74    requestData
     75  );
     76  testMultiPartHeaders(data);
     77 
     78  testGetHeadersFromMultipartText({
     79    postDataText: "Content-Type: text/plain\r\n\r\n",
     80  });
     81 
     82  if (Services.appinfo.OS != "WINNT") {
     83    testEscapeStringPosix();
     84  } else {
     85    testEscapeStringWin();
     86  }
     87 
     88  await teardown(monitor);
     89 });
     90 
     91 function testIsUrlEncodedRequest(data) {
     92  const isUrlEncoded = CurlUtils.isUrlEncodedRequest(data);
     93  ok(isUrlEncoded, "Should return true for url encoded requests.");
     94 }
     95 
     96 function testIsMultipartRequest(data) {
     97  const isMultipart = CurlUtils.isMultipartRequest(data);
     98  ok(isMultipart, "Should return true for multipart/form-data requests.");
     99 }
    100 
    101 function testFindHeader(data) {
    102  const { headers } = data;
    103  const hostName = CurlUtils.findHeader(headers, "Host");
    104  const requestedWithLowerCased = CurlUtils.findHeader(
    105    headers,
    106    "x-requested-with"
    107  );
    108  const doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist");
    109 
    110  is(
    111    hostName,
    112    "example.com",
    113    "Header with name 'Host' should be found in the request array."
    114  );
    115  is(
    116    requestedWithLowerCased,
    117    "XMLHttpRequest",
    118    "The search should be case insensitive."
    119  );
    120  is(doesNotExist, null, "Should return null when a header is not found.");
    121 }
    122 
    123 function testMultiPartHeaders(data) {
    124  const { headers } = data;
    125  const contentType = CurlUtils.findHeader(headers, "Content-Type");
    126 
    127  ok(
    128    contentType.startsWith("multipart/form-data; boundary="),
    129    "Multi-part content type header is present in headers array"
    130  );
    131 }
    132 
    133 function testWritePostDataTextParams(data) {
    134  const params = CurlUtils.writePostDataTextParams(data.postDataText);
    135  is(
    136    params,
    137    "param1=value1&param2=value2&param3=value3",
    138    "Should return a serialized representation of the request parameters"
    139  );
    140 }
    141 
    142 function testWriteEmptyPostDataTextParams() {
    143  const params = CurlUtils.writePostDataTextParams(null);
    144  is(params, "", "Should return a empty string when no parameters provided");
    145 }
    146 
    147 function testDataArgumentOnGeneratedCommand(data) {
    148  const curlCommand = Curl.generateCommand(data);
    149  ok(
    150    curlCommand.includes("--data-raw"),
    151    "Should return a curl command with --data-raw"
    152  );
    153 }
    154 
    155 function testDataEscapeOnGeneratedCommand(data) {
    156  const paramsWin = `--data-raw ^"^{^\\^"param1^\\^":^\\^"value1^\\^",^\\^"param2^\\^":^\\^"value2^\\^"^}^`;
    157  const paramsPosix = `--data-raw '{"param1":"value1","param2":"value2"}'`;
    158 
    159  let curlCommand = Curl.generateCommand(data, "WINNT");
    160  ok(
    161    curlCommand.includes(paramsWin),
    162    "Should return a curl command with --data-raw escaped for Windows systems"
    163  );
    164 
    165  curlCommand = Curl.generateCommand(data, "Linux");
    166  ok(
    167    curlCommand.includes(paramsPosix),
    168    "Should return a curl command with --data-raw escaped for Posix systems"
    169  );
    170 }
    171 
    172 function testGetMultipartBoundary(data) {
    173  const boundary = CurlUtils.getMultipartBoundary(data);
    174  ok(
    175    /-{3,}\w+/.test(boundary),
    176    "A boundary string should be found in a multipart request."
    177  );
    178 }
    179 
    180 function testRemoveBinaryDataFromMultipartText(data) {
    181  const generatedBoundary = CurlUtils.getMultipartBoundary(data);
    182  const text = data.postDataText;
    183  const binaryRemoved = CurlUtils.removeBinaryDataFromMultipartText(
    184    text,
    185    generatedBoundary
    186  );
    187  const boundary = "--" + generatedBoundary;
    188 
    189  const EXPECTED_POSIX_RESULT = [
    190    "$'",
    191    boundary,
    192    "\\r\\n",
    193    'Content-Disposition: form-data; name="param1"',
    194    "\\r\\n\\r\\n",
    195    "value1",
    196    "\\r\\n",
    197    boundary,
    198    "\\r\\n",
    199    'Content-Disposition: form-data; name="file"; filename="filename.png"',
    200    "\\r\\n",
    201    "Content-Type: image/png",
    202    "\\r\\n\\r\\n",
    203    boundary + "--",
    204    "\\r\\n",
    205    "'",
    206  ].join("");
    207 
    208  const EXPECTED_WIN_RESULT = [
    209    '^"',
    210    boundary,
    211    "^\u000A\u000A",
    212    'Content-Disposition: form-data; name=^\\^"param1^\\^"',
    213    "^\u000A\u000A^\u000A\u000A",
    214    "value1",
    215    "^\u000A\u000A",
    216    boundary,
    217    "^\u000A\u000A",
    218    'Content-Disposition: form-data; name=^\\^"file^\\^"; filename=^\\^"filename.png^\\^"',
    219    "^\u000A\u000A",
    220    "Content-Type: image/png",
    221    "^\u000A\u000A^\u000A\u000A",
    222    boundary + "--",
    223    "^\u000A\u000A",
    224    '^"',
    225  ].join("");
    226 
    227  if (Services.appinfo.OS != "WINNT") {
    228    is(
    229      CurlUtils.escapeStringPosix(binaryRemoved),
    230      EXPECTED_POSIX_RESULT,
    231      "The mulitpart request payload should not contain binary data."
    232    );
    233  } else {
    234    is(
    235      CurlUtils.escapeStringWin(binaryRemoved),
    236      EXPECTED_WIN_RESULT,
    237      "WinNT: The mulitpart request payload should not contain binary data."
    238    );
    239  }
    240 }
    241 
    242 function testGetHeadersFromMultipartText(data) {
    243  const headers = CurlUtils.getHeadersFromMultipartText(data.postDataText);
    244 
    245  ok(Array.isArray(headers), "Should return an array.");
    246  ok(!!headers.length, "There should exist at least one request header.");
    247  is(
    248    headers[0].name,
    249    "Content-Type",
    250    "The first header name should be 'Content-Type'."
    251  );
    252 }
    253 
    254 function testEscapeStringPosix() {
    255  const surroundedWithQuotes = "A simple string";
    256  is(
    257    CurlUtils.escapeStringPosix(surroundedWithQuotes),
    258    "'A simple string'",
    259    "The string should be surrounded with single quotes."
    260  );
    261 
    262  const singleQuotes = "It's unusual to put crickets in your coffee.";
    263  is(
    264    CurlUtils.escapeStringPosix(singleQuotes),
    265    "$'It\\'s unusual to put crickets in your coffee.'",
    266    "Single quotes should be escaped."
    267  );
    268 
    269  const escapeChar = "'!ls:q:gs|ls|;ping 8.8.8.8;|";
    270  is(
    271    CurlUtils.escapeStringPosix(escapeChar),
    272    "$'\\'\\041ls:q:gs|ls|;ping 8.8.8.8;|'",
    273    "'!' should be escaped."
    274  );
    275 
    276  const escapeBangOnlyChar = "!";
    277  is(
    278    CurlUtils.escapeStringPosix(escapeBangOnlyChar),
    279    "$'\\041'",
    280    "'!' should be escaped."
    281  );
    282 
    283  const newLines = "Line 1\r\nLine 2\u000d\u000ALine3";
    284  is(
    285    CurlUtils.escapeStringPosix(newLines),
    286    "$'Line 1\\r\\nLine 2\\r\\nLine3'",
    287    "Newlines should be escaped."
    288  );
    289 
    290  const controlChars = "\u0007 \u0009 \u000C \u001B";
    291  is(
    292    CurlUtils.escapeStringPosix(controlChars),
    293    "$'\\x07 \\x09 \\x0c \\x1b'",
    294    "Control characters should be escaped."
    295  );
    296 
    297  // æ ø ü ß ö é
    298  const extendedAsciiChars =
    299    "\xc3\xa6 \xc3\xb8 \xc3\xbc \xc3\x9f \xc3\xb6 \xc3\xa9";
    300  is(
    301    CurlUtils.escapeStringPosix(extendedAsciiChars),
    302    "$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'",
    303    "Character codes outside of the decimal range 32 - 126 should be escaped."
    304  );
    305 }
    306 
    307 function testEscapeStringWin() {
    308  const surroundedWithDoubleQuotes = "A simple string";
    309  is(
    310    CurlUtils.escapeStringWin(surroundedWithDoubleQuotes),
    311    '^\"A simple string^\"',
    312    "The string should be surrounded with double quotes."
    313  );
    314 
    315  const doubleQuotes = 'Quote: "Time is an illusion. Lunchtime doubly so."';
    316  is(
    317    CurlUtils.escapeStringWin(doubleQuotes),
    318    '^\"Quote: ^\\^\"Time is an illusion. Lunchtime doubly so.^\\^\"^\"',
    319    "Double quotes should be escaped."
    320  );
    321 
    322  const percentSigns = "%TEMP% %@foo% %2XX% %_XX% %?XX%";
    323  is(
    324    CurlUtils.escapeStringWin(percentSigns),
    325    '^\"^%^TEMP^% ^%^@foo^% ^%^2XX^% ^%^_XX^% ^%?XX^%^\"',
    326    "Percent signs should be escaped."
    327  );
    328 
    329  const backslashes = "\\A simple string\\";
    330  is(
    331    CurlUtils.escapeStringWin(backslashes),
    332    '^\"^\\A simple string^\\^\"',
    333    "Backslashes should be escaped."
    334  );
    335 
    336  const newLines = "line1\r\nline2\r\rline3\n\nline4";
    337  is(
    338    CurlUtils.escapeStringWin(newLines),
    339    '^\"line1^\n\nline2^\n\n^\n\nline3^\n\n^\n\nline4^\"',
    340    "Newlines should be escaped."
    341  );
    342 
    343  const dollarSignCommand = "$(calc.exe)";
    344  is(
    345    CurlUtils.escapeStringWin(dollarSignCommand),
    346    '^\"^$(calc.exe)^\"',
    347    "Dollar sign should be escaped."
    348  );
    349 
    350  const tickSignCommand = "`$(calc.exe)";
    351  is(
    352    CurlUtils.escapeStringWin(tickSignCommand),
    353    '^\"`^$(calc.exe)^\"',
    354    "Both the tick and dollar signs should be escaped."
    355  );
    356 
    357  const evilCommand = `query=evil\r\rcmd" /c timeout /t 3 & calc.exe\r\r`;
    358  is(
    359    CurlUtils.escapeStringWin(evilCommand),
    360    '^\"query=evil^\n\n^\n\ncmd^\\^\" /c timeout /t 3 ^& calc.exe^\n\n^\n\n^\"',
    361    "The evil command is escaped properly"
    362  );
    363 
    364  // Control characters https://www.ascii-code.com/characters/control-characters
    365  const containsControlChars = " - \u0007 \u0010 \u0014 \u001B \x1a - ";
    366  is(
    367    CurlUtils.escapeStringWin(containsControlChars),
    368    '^\" - \u0007 \u0010 \u0014 \u001b \u001a - ^\"',
    369    "Control characters should not be escaped with ^."
    370  );
    371 
    372  const controlCharsWithWhitespaces = " -\tcalc.exe\f- ";
    373  is(
    374    CurlUtils.escapeStringWin(controlCharsWithWhitespaces),
    375    '^\" - calc.exe - ^\"',
    376    "Control (non-printable) characters which are whitespace like charaters e.g (tab & form feed)"
    377  );
    378 }
    379 
    380 async function createCurlData(selected, getLongString, requestData) {
    381  const { id, url, method, httpVersion } = selected;
    382 
    383  // Create a sanitized object for the Curl command generator.
    384  const data = {
    385    url,
    386    method,
    387    headers: [],
    388    httpVersion,
    389    postDataText: null,
    390  };
    391 
    392  const requestHeaders = await requestData(id, "requestHeaders");
    393  // Fetch header values.
    394  for (const { name, value } of requestHeaders.headers) {
    395    const text = await getLongString(value);
    396    data.headers.push({ name, value: text });
    397  }
    398 
    399  const requestPostData = await requestData(id, "requestPostData");
    400  // Fetch the request payload.
    401  if (requestPostData) {
    402    const postData = requestPostData.postData.text;
    403    data.postDataText = await getLongString(postData);
    404  }
    405 
    406  return data;
    407 }