tor-browser

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

curl.js (16845B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /*
      6 * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
      7 * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
      8 * Copyright (C) 2011 Google Inc. All rights reserved.
      9 * Copyright (C) 2009 Mozilla Foundation. All rights reserved.
     10 *
     11 * Redistribution and use in source and binary forms, with or without
     12 * modification, are permitted provided that the following conditions
     13 * are met:
     14 *
     15 * 1.  Redistributions of source code must retain the above copyright
     16 *     notice, this list of conditions and the following disclaimer.
     17 * 2.  Redistributions in binary form must reproduce the above copyright
     18 *     notice, this list of conditions and the following disclaimer in the
     19 *     documentation and/or other materials provided with the distribution.
     20 * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
     21 *     its contributors may be used to endorse or promote products derived
     22 *     from this software without specific prior written permission.
     23 *
     24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     27 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
     28 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     29 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     30 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     31 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     32 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
     33 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     34 */
     35 
     36 "use strict";
     37 
     38 const Curl = {
     39  /**
     40   * Generates a cURL command string which can be used from the command line etc.
     41   *
     42   * @param object data
     43   *        Datasource to create the command from.
     44   *        The object must contain the following properties:
     45   *        - url:string, the URL of the request.
     46   *        - method:string, the request method upper cased. HEAD / GET / POST etc.
     47   *        - headers:array, an array of request headers {name:x, value:x} tuples.
     48   *        - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1"
     49   *        - postDataText:string, optional - the request payload.
     50   *
     51   * @param string platform
     52   *        Optional parameter to override platform,
     53   *        Fallbacks to current platform if not defined.
     54   *
     55   * @return string
     56   *         A cURL command.
     57   */
     58  generateCommand(data, platform) {
     59    const utils = CurlUtils;
     60 
     61    let commandParts = [];
     62 
     63    // Make sure to use the following helpers to sanitize arguments before execution.
     64    const escapeStringifNeeded = value => {
     65      return /^[a-zA-Z-]+$/.test(value) ? value : escapeString(value);
     66    };
     67 
     68    const ignoredHeaders = new Set();
     69    const currentPlatform = platform || Services.appinfo.OS;
     70 
     71    // The cURL command is expected to run on the same platform that Firefox runs
     72    // (it may be different from the inspected page platform).
     73    const escapeString =
     74      currentPlatform === "WINNT"
     75        ? utils.escapeStringWin
     76        : utils.escapeStringPosix;
     77 
     78    // Add URL.
     79    commandParts.push(escapeString(data.url));
     80 
     81    // Disable globbing if the URL contains brackets.
     82    // cURL also globs braces but they are already percent-encoded.
     83    if (data.url.includes("[") || data.url.includes("]")) {
     84      commandParts.push("--globoff");
     85    }
     86 
     87    let postDataText = null;
     88    const multipartRequest = utils.isMultipartRequest(data);
     89 
     90    // Create post data.
     91    const postData = [];
     92    if (multipartRequest) {
     93      // WINDOWS KNOWN LIMITATIONS: Due to the specificity of running curl on
     94      // cmd.exe even correctly escaped windows newline \r\n will be
     95      // treated by curl as plain local newline. It corresponds in unix
     96      // to single \n and that's what curl will send in payload.
     97      // It may be particularly hurtful for multipart/form-data payloads
     98      // which composed using \n only, not \r\n, may be not parsable for
     99      // peers which split parts of multipart payload using \r\n.
    100      postDataText = data.postDataText;
    101      postData.push("--data-binary");
    102      const boundary = utils.getMultipartBoundary(data);
    103      const text = utils.removeBinaryDataFromMultipartText(
    104        postDataText,
    105        boundary
    106      );
    107      postData.push(escapeStringifNeeded(text));
    108      ignoredHeaders.add("content-length");
    109    } else if (
    110      data.postDataText &&
    111      (utils.isUrlEncodedRequest(data) ||
    112        ["PUT", "POST", "PATCH"].includes(data.method))
    113    ) {
    114      // When no postData exists, --data-raw should not be set
    115      postDataText = data.postDataText;
    116      postData.push(
    117        "--data-raw " +
    118          escapeStringifNeeded(`${utils.writePostDataTextParams(postDataText)}`)
    119      );
    120      ignoredHeaders.add("content-length");
    121    }
    122    // curl generates the host header itself based on the given URL
    123    ignoredHeaders.add("host");
    124 
    125    // Add --compressed if the response is compressed
    126    if (utils.isContentEncodedResponse(data)) {
    127      commandParts.push("--compressed");
    128    }
    129 
    130    // Add -I (HEAD)
    131    // For servers that supports HEAD.
    132    // This will fetch the header of a document only.
    133    if (data.method === "HEAD") {
    134      commandParts.push("-I");
    135    } else if (data.method !== "GET") {
    136      // Add method.
    137      // For HEAD and GET requests this is not necessary. GET is the
    138      // default, -I implies HEAD.
    139      commandParts.push("-X " + escapeStringifNeeded(`${data.method}`));
    140    }
    141 
    142    // Add request headers.
    143    let headers = data.headers;
    144    if (multipartRequest) {
    145      const multipartHeaders = utils.getHeadersFromMultipartText(postDataText);
    146      headers = headers.concat(multipartHeaders);
    147    }
    148    for (let i = 0; i < headers.length; i++) {
    149      const header = headers[i];
    150      if (ignoredHeaders.has(header.name.toLowerCase())) {
    151        continue;
    152      }
    153      commandParts.push(
    154        "-H " + escapeStringifNeeded(`${header.name}: ${header.value}`)
    155      );
    156    }
    157 
    158    // Add post data.
    159    commandParts = commandParts.concat(postData);
    160 
    161    // Format with line breaks if the command has more than 2 parts
    162    // e.g
    163    // Command with 2 parts - curl https://foo.com
    164    // Commands with more than 2 parts -
    165    // curl https://foo.com
    166    // -X POST
    167    // -H "Accept : */*"
    168    // -H "accept-language: en-US"
    169    const joinStr = currentPlatform === "WINNT" ? " ^\n  " : " \\\n  ";
    170    const CMD = currentPlatform == "WINNT" ? "curl.exe " : "curl ";
    171    return CMD + commandParts.join(commandParts.length >= 3 ? joinStr : " ");
    172  },
    173 };
    174 
    175 exports.Curl = Curl;
    176 
    177 /**
    178 * Utility functions for the Curl command generator.
    179 */
    180 const CurlUtils = {
    181  /**
    182   * Check if the request is an URL encoded request.
    183   *
    184   * @param object data
    185   *        The data source. See the description in the Curl object.
    186   * @return boolean
    187   *         True if the request is URL encoded, false otherwise.
    188   */
    189  isUrlEncodedRequest(data) {
    190    let postDataText = data.postDataText;
    191    if (!postDataText) {
    192      return false;
    193    }
    194 
    195    postDataText = postDataText.toLowerCase();
    196    if (
    197      postDataText.includes("content-type: application/x-www-form-urlencoded")
    198    ) {
    199      return true;
    200    }
    201 
    202    const contentType = this.findHeader(data.headers, "content-type");
    203 
    204    return (
    205      contentType &&
    206      contentType.toLowerCase().includes("application/x-www-form-urlencoded")
    207    );
    208  },
    209 
    210  /**
    211   * Check if the request is a multipart request.
    212   *
    213   * @param object data
    214   *        The data source.
    215   * @return boolean
    216   *         True if the request is multipart reqeust, false otherwise.
    217   */
    218  isMultipartRequest(data) {
    219    let postDataText = data.postDataText;
    220    if (!postDataText) {
    221      return false;
    222    }
    223 
    224    postDataText = postDataText.toLowerCase();
    225    if (postDataText.includes("content-type: multipart/form-data")) {
    226      return true;
    227    }
    228 
    229    const contentType = this.findHeader(data.headers, "content-type");
    230 
    231    return (
    232      contentType && contentType.toLowerCase().includes("multipart/form-data;")
    233    );
    234  },
    235 
    236  /**
    237   * Check if the response of an URL has content encoding header.
    238   *
    239   * @param object data
    240   *        The data source. See the description in the Curl object.
    241   * @return boolean
    242   *         True if the response is compressed, false otherwise.
    243   */
    244  isContentEncodedResponse(data) {
    245    return !!this.findHeader(data.responseHeaders, "content-encoding");
    246  },
    247 
    248  /**
    249   * Write out paramters from post data text.
    250   *
    251   * @param object postDataText
    252   *        Post data text.
    253   * @return string
    254   *         Post data parameters.
    255   */
    256  writePostDataTextParams(postDataText) {
    257    if (!postDataText) {
    258      return "";
    259    }
    260    const lines = postDataText.split("\r\n");
    261    return lines[lines.length - 1];
    262  },
    263 
    264  /**
    265   * Finds the header with the given name in the headers array.
    266   *
    267   * @param array headers
    268   *        Array of headers info {name:x, value:x}.
    269   * @param string name
    270   *        The header name to find.
    271   * @return string
    272   *         The found header value or null if not found.
    273   */
    274  findHeader(headers, name) {
    275    if (!headers) {
    276      return null;
    277    }
    278 
    279    name = name.toLowerCase();
    280    for (const header of headers) {
    281      if (name == header.name.toLowerCase()) {
    282        return header.value;
    283      }
    284    }
    285 
    286    return null;
    287  },
    288 
    289  /**
    290   * Returns the boundary string for a multipart request.
    291   *
    292   * @param string data
    293   *        The data source. See the description in the Curl object.
    294   * @return string
    295   *         The boundary string for the request.
    296   */
    297  getMultipartBoundary(data) {
    298    const boundaryRe = /\bboundary=(-{3,}\w+)/i;
    299 
    300    // Get the boundary string from the Content-Type request header.
    301    const contentType = this.findHeader(data.headers, "Content-Type");
    302    if (boundaryRe.test(contentType)) {
    303      return contentType.match(boundaryRe)[1];
    304    }
    305    // Temporary workaround. As of 2014-03-11 the requestHeaders array does not
    306    // always contain the Content-Type header for mulitpart requests. See bug 978144.
    307    // Find the header from the request payload.
    308    const boundaryString = data.postDataText.match(boundaryRe)[1];
    309    if (boundaryString) {
    310      return boundaryString;
    311    }
    312 
    313    return null;
    314  },
    315 
    316  /**
    317   * Removes the binary data from multipart text.
    318   *
    319   * @param string multipartText
    320   *        Multipart form data text.
    321   * @param string boundary
    322   *        The boundary string.
    323   * @return string
    324   *         The multipart text without the binary data.
    325   */
    326  removeBinaryDataFromMultipartText(multipartText, boundary) {
    327    let result = "";
    328    boundary = "--" + boundary;
    329    const parts = multipartText.split(boundary);
    330    for (const part of parts) {
    331      // Each part is expected to have a content disposition line.
    332      let contentDispositionLine = part.trimLeft().split("\r\n")[0];
    333      if (!contentDispositionLine) {
    334        continue;
    335      }
    336      contentDispositionLine = contentDispositionLine.toLowerCase();
    337      if (contentDispositionLine.includes("content-disposition: form-data")) {
    338        if (contentDispositionLine.includes("filename=")) {
    339          // The header lines and the binary blob is separated by 2 CRLF's.
    340          // Add only the headers to the result.
    341          const headers = part.split("\r\n\r\n")[0];
    342          result += boundary + headers + "\r\n\r\n";
    343        } else {
    344          result += boundary + part;
    345        }
    346      }
    347    }
    348    result += boundary + "--\r\n";
    349 
    350    return result;
    351  },
    352 
    353  /**
    354   * Get the headers from a multipart post data text.
    355   *
    356   * @param string multipartText
    357   *        Multipart post text.
    358   * @return array
    359   *         An array of header objects {name:x, value:x}
    360   */
    361  getHeadersFromMultipartText(multipartText) {
    362    const headers = [];
    363    if (!multipartText || multipartText.startsWith("---")) {
    364      return headers;
    365    }
    366 
    367    // Get the header section.
    368    const index = multipartText.indexOf("\r\n\r\n");
    369    if (index == -1) {
    370      return headers;
    371    }
    372 
    373    // Parse the header lines.
    374    const headersText = multipartText.substring(0, index);
    375    const headerLines = headersText.split("\r\n");
    376    let lastHeaderName = null;
    377 
    378    for (const line of headerLines) {
    379      // Create a header for each line in fields that spans across multiple lines.
    380      // Subsquent lines always begins with at least one space or tab character.
    381      // (rfc2616)
    382      if (lastHeaderName && /^\s+/.test(line)) {
    383        headers.push({ name: lastHeaderName, value: line.trim() });
    384        continue;
    385      }
    386 
    387      const indexOfColon = line.indexOf(":");
    388      if (indexOfColon == -1) {
    389        continue;
    390      }
    391 
    392      const header = [
    393        line.slice(0, indexOfColon),
    394        line.slice(indexOfColon + 1),
    395      ];
    396      if (header.length != 2) {
    397        continue;
    398      }
    399      lastHeaderName = header[0].trim();
    400      headers.push({ name: lastHeaderName, value: header[1].trim() });
    401    }
    402 
    403    return headers;
    404  },
    405 
    406  /**
    407   * Escape util function for POSIX oriented operating systems.
    408   * Credit: Google DevTools
    409   */
    410  escapeStringPosix(str) {
    411    function escapeCharacter(x) {
    412      let code = x.charCodeAt(0);
    413      if (code < 256) {
    414        // Add leading zero when needed to not care about the next character.
    415        return code < 16
    416          ? "\\x0" + code.toString(16)
    417          : "\\x" + code.toString(16);
    418      }
    419      code = code.toString(16);
    420      return "\\u" + ("0000" + code).substr(code.length, 4);
    421    }
    422 
    423    // Escape characters which are not within the charater range
    424    // SPACE to "~"(char codes 32 - 126), the `!` (code 33) and '(code 39);
    425    if (/[^\x20-\x7E]|!|\'/.test(str)) {
    426      // Use ANSI-C quoting syntax.
    427      return (
    428        "$'" +
    429        str
    430          .replace(/\\/g, "\\\\")
    431          .replace(/\'/g, "\\'")
    432          .replace(/\n/g, "\\n")
    433          .replace(/\r/g, "\\r")
    434          .replace(/!/g, "\\041")
    435          .replace(/[^\x20-\x7E]/g, escapeCharacter) +
    436        "'"
    437      );
    438    }
    439 
    440    // Use single quote syntax.
    441    return "'" + str + "'";
    442  },
    443 
    444  /**
    445   * Escape util function for Windows systems.
    446   * Credit: Google DevTools
    447   */
    448  escapeStringWin(str) {
    449    /*
    450      Because cmd.exe parser and MS Crt arguments parsers use some of the
    451      same escape characters, they can interact with each other in
    452      horrible ways, the order of operations is critical.
    453    
    454      Also see https://ss64.com/nt/syntax-esc.html for details on
    455      escaping characters on Windows.
    456    */
    457    const encapsChars = '^"';
    458    return (
    459      encapsChars +
    460      str
    461        // Replace all " with \" to ensure the first parser does not remove it.
    462        .replace(/"/g, '\\"')
    463 
    464        // Then escape all characters we are not sure about with ^ to ensure it
    465        // gets to MS Crt parser safely.
    466        // Note: Also do not escape unicode control (C) non-printable characters
    467        // https://www.compart.com/en/unicode/category (this is captured with `\p{C}` and the `u` unicode flag)
    468        .replace(/[^-a-zA-Z0-9\s_:=+~\/.',?;()*`\p{C}]/gu, "^$&")
    469 
    470        // The % character is special because MS Crt parser will try and look for
    471        // ENV variables and fill them in its place. We cannot escape them with %
    472        // and cannot escape them with ^ (because it's cmd.exe's escape not MS Crt
    473        // parser); So we can get cmd.exe parser to escape the character after it,
    474        // if it is followed by a valid beginning character of an ENV variable.
    475        // This ensures we do not try and double escape another ^ if it was placed
    476        // by the previous replace.
    477        .replace(/%(?=[a-zA-Z0-9_])/g, "%^")
    478 
    479        // All other whitespace characters are replaced with a single space, as there
    480        // is no way to enter their literal values in a command line, and they do break
    481        // the command allowing for injection.
    482        // Since want to keep line breaks, we need to exclude them in the regex (`[^\r\n]`),
    483        // and use double negations to get the other whitespace chars (`[^\S]` translates
    484        // to "not not whitespace")
    485        .replace(/[^\S\r\n]/g, " ")
    486 
    487        // Lastly we replace new lines with ^ and TWO new lines because the first
    488        // new line is there to enact the escape command the second is the character
    489        // to escape (in this case new line).
    490        .replace(/\r?\n|\r/g, "^\n\n") +
    491      encapsChars
    492    );
    493  },
    494 };
    495 
    496 exports.CurlUtils = CurlUtils;