tor-browser

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

test_curl.js (10898B)


      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 utility functions contained in `source-utils.js`
      8 */
      9 
     10 const curl = require("resource://devtools/client/shared/curl.js");
     11 const Curl = curl.Curl;
     12 const CurlUtils = curl.CurlUtils;
     13 
     14 // Test `Curl.generateCommand` headers forwarding/filtering
     15 add_task(async function () {
     16  const request = {
     17    url: "https://example.com/form/",
     18    method: "GET",
     19    headers: [
     20      { name: "Host", value: "example.com" },
     21      {
     22        name: "User-Agent",
     23        value:
     24          "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0",
     25      },
     26      { name: "Accept", value: "*/*" },
     27      { name: "Accept-Language", value: "en-US,en;q=0.9" },
     28      { name: "Accept-Encoding", value: "gzip, deflate, br" },
     29      { name: "Origin", value: "https://example.com" },
     30      { name: "Connection", value: "keep-alive" },
     31      { name: "Referer", value: "https://example.com/home/" },
     32      { name: "Content-Type", value: "text/plain" },
     33    ],
     34    responseHeaders: [],
     35    httpVersion: "HTTP/2.0",
     36  };
     37 
     38  const cmd = Curl.generateCommand(request);
     39  const curlParams = parseCurl(cmd);
     40 
     41  ok(
     42    !headerTypeInParams(curlParams, "Host"),
     43    "host header ignored - to be generated from url"
     44  );
     45  ok(
     46    exactHeaderInParams(curlParams, "Accept: */*"),
     47    "accept header present in curl command"
     48  );
     49  ok(
     50    exactHeaderInParams(
     51      curlParams,
     52      "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"
     53    ),
     54    "user-agent header present in curl command"
     55  );
     56  ok(
     57    exactHeaderInParams(curlParams, "Accept-Language: en-US,en;q=0.9"),
     58    "accept-language header present in curl output"
     59  );
     60  ok(
     61    exactHeaderInParams(curlParams, "Accept-Encoding: gzip, deflate, br"),
     62    "accept-encoding header present in curl output"
     63  );
     64  ok(
     65    exactHeaderInParams(curlParams, "Origin: https://example.com"),
     66    "origin header present in curl output"
     67  );
     68  ok(
     69    exactHeaderInParams(curlParams, "Connection: keep-alive"),
     70    "connection header present in curl output"
     71  );
     72  ok(
     73    exactHeaderInParams(curlParams, "Referer: https://example.com/home/"),
     74    "referer header present in curl output"
     75  );
     76  ok(
     77    exactHeaderInParams(curlParams, "Content-Type: text/plain"),
     78    "content-type header present in curl output"
     79  );
     80  ok(!inParams(curlParams, "--data"), "no data param in GET curl output");
     81  ok(
     82    !inParams(curlParams, "--data-raw"),
     83    "no raw data param in GET curl output"
     84  );
     85 });
     86 
     87 // Test `Curl.generateCommand` URL glob handling
     88 add_task(async function () {
     89  let request = {
     90    url: "https://example.com/",
     91    method: "GET",
     92    headers: [],
     93    responseHeaders: [],
     94    httpVersion: "HTTP/2.0",
     95  };
     96 
     97  let cmd = Curl.generateCommand(request);
     98  let curlParams = parseCurl(cmd);
     99 
    100  ok(
    101    !inParams(curlParams, "--globoff"),
    102    "no globoff param in curl output when not needed"
    103  );
    104 
    105  request = {
    106    url: "https://example.com/[]",
    107    method: "GET",
    108    headers: [],
    109    responseHeaders: [],
    110    httpVersion: "HTTP/2.0",
    111  };
    112 
    113  cmd = Curl.generateCommand(request);
    114  curlParams = parseCurl(cmd);
    115 
    116  ok(
    117    inParams(curlParams, "--globoff"),
    118    "globoff param present in curl output when needed"
    119  );
    120 });
    121 
    122 // Test `Curl.generateCommand` data POSTing
    123 add_task(async function () {
    124  const request = {
    125    url: "https://example.com/form/",
    126    method: "POST",
    127    headers: [
    128      { name: "Content-Length", value: "1000" },
    129      { name: "Content-Type", value: "text/plain" },
    130    ],
    131    responseHeaders: [],
    132    httpVersion: "HTTP/2.0",
    133    postDataText: "A piece of plain payload text",
    134  };
    135 
    136  const cmd = Curl.generateCommand(request);
    137  const curlParams = parseCurl(cmd);
    138 
    139  ok(
    140    !headerTypeInParams(curlParams, "Content-Length"),
    141    "content-length header ignored - curl generates new one"
    142  );
    143  ok(
    144    exactHeaderInParams(curlParams, "Content-Type: text/plain"),
    145    "content-type header present in curl output"
    146  );
    147  ok(
    148    inParams(curlParams, "--data-raw"),
    149    '"--data-raw" param present in curl output'
    150  );
    151  ok(
    152    inParams(curlParams, `--data-raw ${quote(request.postDataText)}`),
    153    "proper payload data present in output"
    154  );
    155 });
    156 
    157 // Test `Curl.generateCommand` data POSTing - not post data
    158 add_task(async function () {
    159  const request = {
    160    url: "https://example.com/form/",
    161    method: "POST",
    162    headers: [
    163      { name: "Content-Length", value: "1000" },
    164      { name: "Content-Type", value: "text/plain" },
    165    ],
    166    responseHeaders: [],
    167    httpVersion: "HTTP/2.0",
    168  };
    169 
    170  const cmd = Curl.generateCommand(request);
    171  const curlParams = parseCurl(cmd);
    172 
    173  ok(
    174    !inParams(curlParams, "--data-raw"),
    175    '"--data-raw" param not present in curl output'
    176  );
    177 
    178  const methodIndex = curlParams.indexOf("-X");
    179 
    180  ok(
    181    methodIndex !== -1 && curlParams[methodIndex + 1] === "POST",
    182    "request method explicit is POST"
    183  );
    184 });
    185 
    186 // Test `Curl.generateCommand` multipart data POSTing
    187 add_task(async function () {
    188  const boundary = "----------14808";
    189  const request = {
    190    url: "https://example.com/form/",
    191    method: "POST",
    192    headers: [
    193      {
    194        name: "Content-Type",
    195        value: `multipart/form-data; boundary=${boundary}`,
    196      },
    197    ],
    198    responseHeaders: [],
    199    httpVersion: "HTTP/2.0",
    200    postDataText: [
    201      `--${boundary}`,
    202      'Content-Disposition: form-data; name="field_one"',
    203      "",
    204      "value_one",
    205      `--${boundary}`,
    206      'Content-Disposition: form-data; name="field_two"',
    207      "",
    208      "value two",
    209      `--${boundary}--`,
    210      "",
    211    ].join("\r\n"),
    212  };
    213 
    214  const cmd = Curl.generateCommand(request);
    215 
    216  // Check content type
    217  const contentTypePos = cmd.indexOf(headerParamPrefix("Content-Type"));
    218  const contentTypeParam = headerParam(
    219    `Content-Type: multipart/form-data; boundary=${boundary}`
    220  );
    221  Assert.notStrictEqual(
    222    contentTypePos,
    223    -1,
    224    "content type header present in curl output"
    225  );
    226  equal(
    227    cmd.substr(contentTypePos, contentTypeParam.length),
    228    contentTypeParam,
    229    "proper content type header present in curl output"
    230  );
    231 
    232  // Check binary data
    233  const dataBinaryPos = cmd.indexOf("--data-binary");
    234  const dataBinaryParam = `--data-binary ${isWin() ? "^\n  " : "\\\n  $"}${escapeNewline(
    235    quote(request.postDataText)
    236  )}`;
    237 
    238  Assert.notStrictEqual(
    239    dataBinaryPos,
    240    -1,
    241    "--data-binary param present in curl output"
    242  );
    243  equal(
    244    cmd.substr(dataBinaryPos, dataBinaryParam.length),
    245    dataBinaryParam,
    246    "proper multipart data present in curl output"
    247  );
    248 });
    249 
    250 // Test `CurlUtils.removeBinaryDataFromMultipartText` doesn't change text data
    251 add_task(async function () {
    252  const boundary = "----------14808";
    253  const postTextLines = [
    254    `--${boundary}`,
    255    'Content-Disposition: form-data; name="field_one"',
    256    "",
    257    "value_one",
    258    `--${boundary}`,
    259    'Content-Disposition: form-data; name="field_two"',
    260    "",
    261    "value two",
    262    `--${boundary}--`,
    263    "",
    264  ];
    265 
    266  const cleanedText = CurlUtils.removeBinaryDataFromMultipartText(
    267    postTextLines.join("\r\n"),
    268    boundary
    269  );
    270  equal(
    271    cleanedText,
    272    postTextLines.join("\r\n"),
    273    "proper non-binary multipart text unchanged"
    274  );
    275 });
    276 
    277 // Test `CurlUtils.removeBinaryDataFromMultipartText` removes binary data
    278 add_task(async function () {
    279  const boundary = "----------14808";
    280  const postTextLines = [
    281    `--${boundary}`,
    282    'Content-Disposition: form-data; name="field_one"',
    283    "",
    284    "value_one",
    285    `--${boundary}`,
    286    'Content-Disposition: form-data; name="field_two"; filename="file_field_two.txt"',
    287    "",
    288    "file content",
    289    `--${boundary}--`,
    290    "",
    291  ];
    292 
    293  const cleanedText = CurlUtils.removeBinaryDataFromMultipartText(
    294    postTextLines.join("\r\n"),
    295    boundary
    296  );
    297  postTextLines.splice(7, 1);
    298  equal(
    299    cleanedText,
    300    postTextLines.join("\r\n"),
    301    "file content removed from multipart text"
    302  );
    303 });
    304 
    305 // Test `Curl.generateCommand` add --compressed flag
    306 add_task(async function () {
    307  let request = {
    308    url: "https://example.com/",
    309    method: "GET",
    310    headers: [],
    311    responseHeaders: [],
    312    httpVersion: "HTTP/2.0",
    313  };
    314 
    315  let cmd = Curl.generateCommand(request);
    316  let curlParams = parseCurl(cmd);
    317 
    318  ok(
    319    !inParams(curlParams, "--compressed"),
    320    "no compressed param in curl output when not needed"
    321  );
    322 
    323  request = {
    324    url: "https://example.com/",
    325    method: "GET",
    326    headers: [],
    327    responseHeaders: [{ name: "Content-Encoding", value: "gzip" }],
    328    httpVersion: "HTTP/2.0",
    329  };
    330 
    331  cmd = Curl.generateCommand(request);
    332  curlParams = parseCurl(cmd);
    333 
    334  ok(
    335    inParams(curlParams, "--compressed"),
    336    "compressed param present in curl output when needed"
    337  );
    338 });
    339 
    340 function isWin() {
    341  return Services.appinfo.OS === "WINNT";
    342 }
    343 
    344 const QUOTE = isWin() ? '^"' : "'";
    345 
    346 // Quote a string, escape the quotes inside the string
    347 function quote(str) {
    348  let escaped;
    349  if (isWin()) {
    350    escaped = str.replace(new RegExp('"', "g"), `^\\${QUOTE}`);
    351  } else {
    352    escaped = str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`);
    353  }
    354  return QUOTE + escaped + QUOTE;
    355 }
    356 
    357 function escapeNewline(txt) {
    358  if (isWin()) {
    359    // For windows we replace new lines with ^ and TWO new lines because the first
    360    // new line is there to enact the escape command the second is the character
    361    // to escape (in this case new line).
    362    return txt.replace(/\r?\n|\r/g, "^\n\n");
    363  }
    364  return txt.replace(/\r/g, "\\r").replace(/\n/g, "\\n");
    365 }
    366 
    367 // Header param is formatted as -H "Header: value" or -H 'Header: value'
    368 function headerParam(h) {
    369  return "-H " + quote(h);
    370 }
    371 
    372 // Header param prefix is formatted as `-H "HeaderName` or `-H 'HeaderName`
    373 function headerParamPrefix(headerName) {
    374  return `-H ${QUOTE}${headerName}`;
    375 }
    376 
    377 // If any params startswith `-H "HeaderName` or `-H 'HeaderName`
    378 function headerTypeInParams(curlParams, headerName) {
    379  return curlParams.some(param =>
    380    param.toLowerCase().startsWith(headerParamPrefix(headerName).toLowerCase())
    381  );
    382 }
    383 
    384 function exactHeaderInParams(curlParams, header) {
    385  return curlParams.some(param => param === headerParam(header));
    386 }
    387 
    388 function inParams(curlParams, param) {
    389  return curlParams.some(p => p.startsWith(param));
    390 }
    391 
    392 // Parse complete curl command to array of params. Can be applied to simple headers/data,
    393 // but will not on WIN with sophisticated values of --data-binary with e.g. escaped quotes
    394 function parseCurl(curlCmd) {
    395  // This monster regexp parses the command line into an array of arguments,
    396  // recognizing quoted args with matching quotes and escaped quotes inside:
    397  // [ "curl.exe 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
    398  // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
    399  const matchRe = /[-\.A-Za-z1-9]+(?: ([\^\"']+)(?:\\\1|.)*?\1)?/g;
    400  return curlCmd.match(matchRe);
    401 }