Commit cb84138d authored by Hubert Boma Manilla's avatar Hubert Boma Manilla Committed by rvandermeulen@mozilla.com
Browse files

Bug 1950001 - [devtools] Fix Copy As cURL(Windows) on linux a=RyanVM

parent c2ef9364
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ add_task(async function () {
  info("Starting test... ");

  // Different quote chars are used for Windows and POSIX
  const QUOTE_WIN = '"';
  const QUOTE_WIN = '^"';
  const QUOTE_POSIX = "'";

  const isWin = Services.appinfo.OS === "WINNT";
@@ -219,7 +219,7 @@ async function testForPlatform(tab, monitor, testData) {
        // This monster regexp parses the command line into an array of arguments,
        // recognizing quoted args with matching quotes and escaped quotes inside:
        // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
        const matchRe = /[-A-Za-z1-9]+(?: ([\"'])(?:\\\1|.)*?\1)?/g;
        const matchRe = /[-A-Za-z1-9]+(?: ([\^\\"']+)(?:\\\1|.)*?\1)?/g;

        const actual = result.match(matchRe);
        // Must begin with the same "curl 'URL'" segment
+20 −20
Original line number Diff line number Diff line
@@ -153,7 +153,7 @@ function testDataArgumentOnGeneratedCommand(data) {
}

function testDataEscapeOnGeneratedCommand(data) {
  const paramsWin = `--data-raw "{""param1"":""value1"",""param2"":""value2""}"`;
  const paramsWin = `--data-raw ^"{\\"param1\\":\\"value1\\",\\"param2\\":\\"value2\\"}^"`;
  const paramsPosix = `--data-raw '{"param1":"value1","param2":"value2"}'`;

  let curlCommand = Curl.generateCommand(data, "WINNT");
@@ -206,22 +206,22 @@ function testRemoveBinaryDataFromMultipartText(data) {
  ].join("");

  const EXPECTED_WIN_RESULT = [
    '"',
    '^"',
    boundary,
    '"^\u000d\u000A\u000d\u000A"',
    'Content-Disposition: form-data; name=""param1""',
    '"^\u000d\u000A\u000d\u000A""^\u000d\u000A\u000d\u000A"',
    "^\u000A\u000A",
    'Content-Disposition: form-data; name=\\"param1\\"',
    "^\u000A\u000A^\u000A\u000A",
    "value1",
    '"^\u000d\u000A\u000d\u000A"',
    "^\u000A\u000A",
    boundary,
    '"^\u000d\u000A\u000d\u000A"',
    'Content-Disposition: form-data; name=""file""; filename=""filename.png""',
    '"^\u000d\u000A\u000d\u000A"',
    "^\u000A\u000A",
    'Content-Disposition: form-data; name=\\"file\\"; filename=\\"filename.png\\"',
    "^\u000A\u000A",
    "Content-Type: image/png",
    '"^\u000d\u000A\u000d\u000A""^\u000d\u000A\u000d\u000A"',
    "^\u000A\u000A^\u000A\u000A",
    boundary + "--",
    '"^\u000d\u000A\u000d\u000A"',
    '"',
    "^\u000A\u000A",
    '^"',
  ].join("");

  if (Services.appinfo.OS != "WINNT") {
@@ -316,56 +316,56 @@ function testEscapeStringWin() {
  const surroundedWithDoubleQuotes = "A simple string";
  is(
    CurlUtils.escapeStringWin(surroundedWithDoubleQuotes),
    '"A simple string"',
    '^"A simple string^"',
    "The string should be surrounded with double quotes."
  );

  const doubleQuotes = 'Quote: "Time is an illusion. Lunchtime doubly so."';
  is(
    CurlUtils.escapeStringWin(doubleQuotes),
    '"Quote: ""Time is an illusion. Lunchtime doubly so."""',
    '^"Quote: \\"Time is an illusion. Lunchtime doubly so.\\"^"',
    "Double quotes should be escaped."
  );

  const percentSigns = "%TEMP% %@foo% %2XX% %_XX% %?XX%";
  is(
    CurlUtils.escapeStringWin(percentSigns),
    '"^%^TEMP^% ^%^@foo^% ^%^2XX^% ^%^_XX^% ^%?XX^%"',
    '^"^%^TEMP^% ^%^@foo^% ^%^2XX^% ^%^_XX^% ^%?XX^%^"',
    "Percent signs should be escaped."
  );

  const backslashes = "\\A simple string\\";
  is(
    CurlUtils.escapeStringWin(backslashes),
    '"\\\\A simple string\\\\"',
    '^"\\\\A simple string\\\\^"',
    "Backslashes should be escaped."
  );

  const newLines = "line1\r\nline2\r\rline3\n\nline4";
  is(
    CurlUtils.escapeStringWin(newLines),
    '"line1"^\r\n\r\n"line2"^\r\n\r\n""^\r\n\r\n"line3"^\r\n\r\n""^\r\n\r\n"line4"',
    '^"line1^\n\nline2\r\rline3^\n\n^\n\nline4^"',
    "Newlines should be escaped."
  );

  const dollarSignCommand = "$(calc.exe)";
  is(
    CurlUtils.escapeStringWin(dollarSignCommand),
    '"\\$(calc.exe)"',
    '^"\\$(calc.exe)^"',
    "Dollar sign should be escaped."
  );

  const tickSignCommand = "`$(calc.exe)";
  is(
    CurlUtils.escapeStringWin(tickSignCommand),
    '"\\`\\$(calc.exe)"',
    '^"\\`\\$(calc.exe)^"',
    "Both the tick and dollar signs should be escaped."
  );

  const evilCommand = `query=evil\r\rcmd" /c timeout /t 3 & calc.exe\r\r`;
  is(
    CurlUtils.escapeStringWin(evilCommand),
    '"query=evil"^\r\n\r\n""^\r\n\r\n"cmd"" /c timeout /t 3 & calc.exe"^\r\n\r\n""^\r\n\r\n""',
    '^"query=evil\r\rcmd\\" /c timeout /t 3 & calc.exe\r\r^"',
    "The evil command is escaped properly"
  );
}
+36 −36
Original line number Diff line number Diff line
@@ -58,17 +58,11 @@ const Curl = {
  generateCommand(data, platform) {
    const utils = CurlUtils;

    let command = ["curl"];
    let commandParts = [];

    // Make sure to use the following helpers to sanitize arguments before execution.
    const addParam = value => {
      const safe = /^[a-zA-Z-]+$/.test(value) ? value : escapeString(value);
      command.push(safe);
    };

    const addPostData = value => {
      const safe = /^[a-zA-Z-]+$/.test(value) ? value : escapeString(value);
      postData.push(safe);
    const escapeStringifNeeded = value => {
      return /^[a-zA-Z-]+$/.test(value) ? value : escapeString(value);
    };

    const ignoredHeaders = new Set();
@@ -77,17 +71,17 @@ const Curl = {
    // The cURL command is expected to run on the same platform that Firefox runs
    // (it may be different from the inspected page platform).
    const escapeString =
      currentPlatform == "WINNT"
      currentPlatform === "WINNT"
        ? utils.escapeStringWin
        : utils.escapeStringPosix;

    // Add URL.
    addParam(data.url);
    commandParts.push(escapeString(data.url));

    // Disable globbing if the URL contains brackets.
    // cURL also globs braces but they are already percent-encoded.
    if (data.url.includes("[") || data.url.includes("]")) {
      addParam("--globoff");
      commandParts.push("--globoff");
    }

    let postDataText = null;
@@ -104,13 +98,13 @@ const Curl = {
      // which composed using \n only, not \r\n, may be not parsable for
      // peers which split parts of multipart payload using \r\n.
      postDataText = data.postDataText;
      addPostData("--data-binary");
      postData.push("--data-binary");
      const boundary = utils.getMultipartBoundary(data);
      const text = utils.removeBinaryDataFromMultipartText(
        postDataText,
        boundary
      );
      addPostData(text);
      postData.push(escapeStringifNeeded(text));
      ignoredHeaders.add("content-length");
    } else if (
      data.postDataText &&
@@ -119,8 +113,10 @@ const Curl = {
    ) {
      // When no postData exists, --data-raw should not be set
      postDataText = data.postDataText;
      addPostData("--data-raw");
      addPostData(utils.writePostDataTextParams(postDataText));
      postData.push(
        "--data-raw " +
          escapeStringifNeeded(`${utils.writePostDataTextParams(postDataText)}`)
      );
      ignoredHeaders.add("content-length");
    }
    // curl generates the host header itself based on the given URL
@@ -128,20 +124,19 @@ const Curl = {

    // Add --compressed if the response is compressed
    if (utils.isContentEncodedResponse(data)) {
      addParam("--compressed");
      commandParts.push("--compressed");
    }

    // Add -I (HEAD)
    // For servers that supports HEAD.
    // This will fetch the header of a document only.
    if (data.method === "HEAD") {
      addParam("-I");
      commandParts.push("-I");
    } else if (data.method !== "GET") {
      // Add method.
      // For HEAD and GET requests this is not necessary. GET is the
      // default, -I implies HEAD.
      addParam("-X");
      addParam(data.method);
      commandParts.push("-X " + escapeStringifNeeded(`${data.method}`));
    }

    // Add request headers.
@@ -155,14 +150,26 @@ const Curl = {
      if (ignoredHeaders.has(header.name.toLowerCase())) {
        continue;
      }
      addParam("-H");
      addParam(header.name + ": " + header.value);
      commandParts.push(
        "-H " + escapeStringifNeeded(`${header.name}: ${header.value}`)
      );
    }

    // Add post data.
    command = command.concat(postData);

    return command.join(" ");
    commandParts = commandParts.concat(postData);

    // Format with line breaks if the command has more than 2 parts
    // e.g
    // Command with 2 parts  - curl https://foo.com
    // Commands with more than 2 parts -
    // curl https://foo.com
    // -X POST
    // -H "Accept : */*"
    // -H "accept-language: en-US"
    const joinStr = currentPlatform === "WINNT" ? " ^\n  " : " \\\n  ";
    return (
      "curl " + commandParts.join(commandParts.length >= 3 ? joinStr : " ")
    );
  },
};

@@ -447,18 +454,16 @@ const CurlUtils = {
      same escape characters, they can interact with each other in
      horrible ways, the order of operations is critical.
    */
    const encapsChars = '"';
    const encapsChars = '^"';
    return (
      encapsChars +
      str

        //  Replace \ with \\ first because it is an escape character for certain
        // conditions in both parsers.
        .replace(/\\/g, "\\\\")

        // Replace double quote chars with two double quotes (not by escaping with \") because it is
        // recognized by both cmd.exe and MS Crt arguments parser.
        .replace(/"/g, '""')
        // Escape double quotes with double slashes.
        .replace(/"/g, '\\"')

        // Escape ` and $ so commands do not get executed e.g $(calc.exe) or `\$(calc.exe)
        .replace(/[`$]/g, "\\$&")
@@ -476,15 +481,10 @@ const CurlUtils = {
        // by the previous replace.
        .replace(/%(?=[a-zA-Z0-9_])/g, "%^")

        // We replace \r and \r\n with \n, this allows to consistently escape all new
        // lines in the next replace
        .replace(/\r\n?/g, "\n")

        // Lastly we replace new lines with ^ and TWO new lines because the first
        // new line is there to enact the escape command the second is the character
        // to escape (in this case new line).
        // The extra " enables escaping new lines with ^ within quotes in cmd.exe.
        .replace(/\n/g, '"^\r\n\r\n"') +
        .replace(/\r?\n/g, "^\n\n") +
      encapsChars
    );
  },
+11 −6
Original line number Diff line number Diff line
@@ -227,9 +227,10 @@ add_task(async function () {

  // Check binary data
  const dataBinaryPos = cmd.indexOf("--data-binary");
  const dataBinaryParam = `--data-binary ${isWin() ? "" : "$"}${escapeNewline(
  const dataBinaryParam = `--data-binary ${isWin() ? "^\n  " : "\\\n  $"}${escapeNewline(
    quote(request.postDataText)
  )}`;

  ok(dataBinaryPos !== -1, "--data-binary param present in curl output");
  equal(
    cmd.substr(dataBinaryPos, dataBinaryParam.length),
@@ -332,13 +333,15 @@ function isWin() {
  return Services.appinfo.OS === "WINNT";
}

const QUOTE = isWin() ? '"' : "'";
const QUOTE = isWin() ? '^"' : "'";

// Quote a string, escape the quotes inside the string
function quote(str) {
  let escaped;
  if (isWin()) {
    escaped = str.replace(new RegExp(QUOTE, "g"), `${QUOTE}${QUOTE}`);
    escaped = str
      .replace(new RegExp(QUOTE, "g"), `${QUOTE}${QUOTE}`)
      .replace(/"/g, '\\"');
  } else {
    escaped = str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`);
  }
@@ -347,8 +350,10 @@ function quote(str) {

function escapeNewline(txt) {
  if (isWin()) {
    // Add `"` to close quote, then escape newline outside of quote, then start new quote
    return txt.replace(/[\r\n]{1,2}/g, '"^$&$&"');
    // Replace new lines with ^ and TWO new lines because the first
    // new line is there to enact the escape command the second is the character
    // to escape (in this case new line).
    return txt.replace(/\r?\n/g, "^\n\n");
  }
  return txt.replace(/\r/g, "\\r").replace(/\n/g, "\\n");
}
@@ -384,6 +389,6 @@ function parseCurl(curlCmd) {
  // This monster regexp parses the command line into an array of arguments,
  // recognizing quoted args with matching quotes and escaped quotes inside:
  // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
  const matchRe = /[-A-Za-z1-9]+(?: \$?([\"'])(?:\\\1|.)*?\1)?/g;
  const matchRe = /[-A-Za-z1-9]+(?: ([\^\\"']+)(?:\\\1|.)*?\1)?/g;
  return curlCmd.match(matchRe);
}