/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2008, 2009 Anthony Ricaud * Copyright (C) 2011 Google Inc. All rights reserved. * Copyright (C) 2009 Mozilla Foundation. All rights reserved. * Copyright (C) 2022, 2023, 2025 Moonchild Productions. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ "use strict"; const Services = require("Services"); const DEFAULT_HTTP_VERSION = "HTTP/1.1"; const Curl = { /** * Generates a cURL command string which can be used from the command line etc. * * @param object data * Datasource to create the command from. * The object must contain the following properties: * - url:string, the URL of the request. * - method:string, the request method upper cased. HEAD / GET / POST etc. * - headers:array, an array of request headers {name:x, value:x} tuples. * - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1" * - postDataText:string, optional - the request payload. * * @return string * A cURL command. */ generateCommand: function (data) { const utils = CurlUtils; let commandParts = []; // Make sure to use the following helpers to sanitize arguments before execution. const escapeStringifNeeded = value => { return /^[a-zA-Z-]+$/.test(value) ? value : escapeString(value); }; let ignoredHeaders = new Set(); // The cURL command is expected to run on the same platform that Firefox runs // (it may be different from the inspected page platform). let escapeString = Services.appinfo.OS == "WINNT" ? utils.escapeStringWin : utils.escapeStringPosix; // Add URL. commandParts.push(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("]")) { commandParts.push("--globoff"); } let postDataText = null; let multipartRequest = utils.isMultipartRequest(data); // Create post data. let postData = []; if (utils.isUrlEncodedRequest(data) || ["PUT", "POST", "PATCH"].includes(data.method)) { postDataText = data.postDataText; postData.push("--data-raw"); let text =utils.writePostDataTextParams(postDataText); postData.push(escapeStringifNeeded(text)); ignoredHeaders.add("content-length"); } else if (multipartRequest) { postDataText = data.postDataText; postData.push("--data-binary"); let boundary = utils.getMultipartBoundary(data); let text = utils.removeBinaryDataFromMultipartText(postDataText, boundary); postData.push(escapeStringifNeeded(text)); ignoredHeaders.add("content-length"); } // Add method. // For GET and POST requests this is not necessary as GET is the // default. If --data or --binary is added POST is the default. if (!(data.method == "GET" || data.method == "POST")) { commandParts.push("-X"); commandParts.push(data.method); } // Add -I (HEAD) // For servers that supports HEAD. // This will fetch the header of a document only. if (data.method == "HEAD") { commandParts.push("-I"); } // Add http version. if (data.httpVersion && data.httpVersion != DEFAULT_HTTP_VERSION) { let version = data.httpVersion.split("/")[1]; // curl accepts --http1.0, --http1.1 and --http2 for HTTP/1.0, HTTP/1.1 // and HTTP/2 protocols respectively. But the corresponding values in // data.httpVersion are HTTP/1.0, HTTP/1.1 and HTTP/2.0 // So in case of HTTP/2.0 (which should ideally be HTTP/2) we are using // only major version, and full version in other cases commandParts.push("--http" + (version == "2.0" ? version.split(".")[0] : version)); } // Add request headers. let headers = data.headers; if (multipartRequest) { let multipartHeaders = utils.getHeadersFromMultipartText(postDataText); headers = headers.concat(multipartHeaders); } for (let i = 0; i < headers.length; i++) { let header = headers[i]; if (header.name.toLowerCase() === "accept-encoding") { // Ignore transfer encoding (compression) as not all commonly installed // versions of curl support this. continue; } if (ignoredHeaders.has(header.name.toLowerCase())) { continue; } commandParts.push("-H"); let text = header.name + ": " + header.value; commandParts.push(escapeStringifNeeded(text)); } // Add post data. 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 = Services.appinfo.OS == "WINNT" ? " ^\n " : " \\\n "; return ( "curl " + commandParts.join(commandParts.length >= 3 ? joinStr : " ") ); } }; exports.Curl = Curl; /** * Utility functions for the Curl command generator. */ const CurlUtils = { /** * Check if the request is an URL encoded request. * * @param object data * The data source. See the description in the Curl object. * @return boolean * True if the request is URL encoded, false otherwise. */ isUrlEncodedRequest: function (data) { let postDataText = data.postDataText; if (!postDataText) { return false; } postDataText = postDataText.toLowerCase(); if (postDataText.includes("content-type: application/x-www-form-urlencoded")) { return true; } let contentType = this.findHeader(data.headers, "content-type"); return (contentType && contentType.toLowerCase().includes("application/x-www-form-urlencoded")); }, /** * Check if the request is a multipart request. * * @param object data * The data source. * @return boolean * True if the request is multipart reqeust, false otherwise. */ isMultipartRequest: function (data) { let postDataText = data.postDataText; if (!postDataText) { return false; } postDataText = postDataText.toLowerCase(); if (postDataText.includes("content-type: multipart/form-data")) { return true; } let contentType = this.findHeader(data.headers, "content-type"); return (contentType && contentType.toLowerCase().includes("multipart/form-data;")); }, /** * Write out paramters from post data text. * * @param object postDataText * Post data text. * @return string * Post data parameters. */ writePostDataTextParams: function (postDataText) { if (!postDataText) { return ""; } let lines = postDataText.split("\r\n"); return lines[lines.length - 1]; }, /** * Finds the header with the given name in the headers array. * * @param array headers * Array of headers info {name:x, value:x}. * @param string name * The header name to find. * @return string * The found header value or null if not found. */ findHeader: function (headers, name) { if (!headers) { return null; } name = name.toLowerCase(); for (let header of headers) { if (name == header.name.toLowerCase()) { return header.value; } } return null; }, /** * Returns the boundary string for a multipart request. * * @param string data * The data source. See the description in the Curl object. * @return string * The boundary string for the request. */ getMultipartBoundary: function (data) { let boundaryRe = /\bboundary=(-{3,}\w+)/i; // Get the boundary string from the Content-Type request header. let contentType = this.findHeader(data.headers, "Content-Type"); if (boundaryRe.test(contentType)) { return contentType.match(boundaryRe)[1]; } // Temporary workaround. As of 2014-03-11 the requestHeaders array does not // always contain the Content-Type header for mulitpart requests. See bug 978144. // Find the header from the request payload. let boundaryString = data.postDataText.match(boundaryRe)[1]; if (boundaryString) { return boundaryString; } return null; }, /** * Removes the binary data from multipart text. * * @param string multipartText * Multipart form data text. * @param string boundary * The boundary string. * @return string * The multipart text without the binary data. */ removeBinaryDataFromMultipartText: function (multipartText, boundary) { let result = ""; boundary = "--" + boundary; let parts = multipartText.split(boundary); for (let part of parts) { // Each part is expected to have a content disposition line. let contentDispositionLine = part.trimLeft().split("\r\n")[0]; if (!contentDispositionLine) { continue; } contentDispositionLine = contentDispositionLine.toLowerCase(); if (contentDispositionLine.includes("content-disposition: form-data")) { if (contentDispositionLine.includes("filename=")) { // The header lines and the binary blob is separated by 2 CRLF's. // Add only the headers to the result. let headers = part.split("\r\n\r\n")[0]; result += boundary + "\r\n" + headers + "\r\n\r\n"; } else { result += boundary + "\r\n" + part; } } } result += boundary + "--\r\n"; return result; }, /** * Get the headers from a multipart post data text. * * @param string multipartText * Multipart post text. * @return array * An array of header objects {name:x, value:x} */ getHeadersFromMultipartText: function (multipartText) { let headers = []; if (!multipartText || multipartText.startsWith("---")) { return headers; } // Get the header section. let index = multipartText.indexOf("\r\n\r\n"); if (index == -1) { return headers; } // Parse the header lines. let headersText = multipartText.substring(0, index); let headerLines = headersText.split("\r\n"); let lastHeaderName = null; for (let line of headerLines) { // Create a header for each line in fields that spans across multiple lines. // Subsquent lines always begins with at least one space or tab character. // (rfc2616) if (lastHeaderName && /^\s+/.test(line)) { headers.push({ name: lastHeaderName, value: line.trim() }); continue; } let indexOfColon = line.indexOf(":"); if (indexOfColon == -1) { continue; } let header = [line.slice(0, indexOfColon), line.slice(indexOfColon + 1)]; if (header.length != 2) { continue; } lastHeaderName = header[0].trim(); headers.push({ name: lastHeaderName, value: header[1].trim() }); } return headers; }, /** * Escape util function for POSIX oriented operating systems. * Credit: Google DevTools */ escapeStringPosix: function (str) { function escapeCharacter(x) { let code = x.charCodeAt(0); if (code < 256) { // Add leading zero when needed to not care about the next character. return code < 16 ? "\\x0" + code.toString(16) : "\\x" + code.toString(16); } code = code.toString(16); return "\\u" + ("0000" + code).substr(code.length, 4); } if (/[^\x20-\x7E]|\'/.test(str)) { // Use ANSI-C quoting syntax. return "$\'" + str.replace(/\\/g, "\\\\") .replace(/\'/g, "\\\'") .replace(/\n/g, "\\n") .replace(/\r/g, "\\r") .replace(/!/g, "\\041") .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'"; } // Use single quote syntax. return "'" + str + "'"; }, /** * Escape util function for Windows systems. */ escapeStringWin: function (str) { 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 ` and $ so commands do not get executed e.g $(calc.exe) or `\$(calc.exe) .replace(/[`$]/g, "\\$&") // Then escape all characters we are not sure about with ^ to ensure it // gets to the MS CRT parser safely. // Note: Do not escape unicode control non-printable characters (0000-001f + 007f-009f) .replace(/[^a-zA-Z0-9\s_\-:=+~\/.',?;()*\$&\\{}\"`\u0000-\u001f\u007f-\u009f]/g, "^$&") // The % character is special because MS CRT parser will try and look for // ENV variables and fill them in its place. We cannot escape them with % // and cannot escape them with ^ (because it's cmd.exe's escape not MS CRT // parser); So we can get cmd.exe parser to escape the character after it, // if it is followed by a valid beginning character of an ENV variable. // This ensures we do not try and double escape another ^ if it was placed // by the previous replace. .replace(/%(?=[a-zA-Z0-9_])/g, "%^") // All other whitespace characters are replaced with a single space, as there // is no way to enter their literal values in a command line, and they do break // the command sequence, allowing for injection. // Since want to keep line breaks, we need to exclude them in the regex (`[^\r\n]`), // and use double negations to get the other whitespace chars (`[^\S]` translates // to "not not whitespace") .replace(/[^\S\r\n]/g, " ") // 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). .replace(/\r?\n|\r/g, "^\n\n") + encapsChars ); } }; exports.CurlUtils = CurlUtils;