diff options
Diffstat (limited to 'devtools/client/shared/curl.js')
-rw-r--r-- | devtools/client/shared/curl.js | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/devtools/client/shared/curl.js b/devtools/client/shared/curl.js new file mode 100644 index 0000000000..47d2aacfe8 --- /dev/null +++ b/devtools/client/shared/curl.js @@ -0,0 +1,489 @@ +/* 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 <rik@webkit.org> + * Copyright (C) 2011 Google Inc. All rights reserved. + * Copyright (C) 2009 Mozilla Foundation. 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 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. + * + * @param string platform + * Optional parameter to override platform, + * Fallbacks to current platform if not defined. + * + * @return string + * A cURL command. + */ + generateCommand(data, platform) { + const utils = CurlUtils; + + let command = ["curl"]; + + // 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 ignoredHeaders = new Set(); + const currentPlatform = platform || Services.appinfo.OS; + + // 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" + ? utils.escapeStringWin + : utils.escapeStringPosix; + + // Add URL. + addParam(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"); + } + + let postDataText = null; + const multipartRequest = utils.isMultipartRequest(data); + + // Create post data. + const postData = []; + if (multipartRequest) { + // WINDOWS KNOWN LIMITATIONS: Due to the specificity of running curl on + // cmd.exe even correctly escaped windows newline \r\n will be + // treated by curl as plain local newline. It corresponds in unix + // to single \n and that's what curl will send in payload. + // It may be particularly hurtful for multipart/form-data payloads + // 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"); + const boundary = utils.getMultipartBoundary(data); + const text = utils.removeBinaryDataFromMultipartText( + postDataText, + boundary + ); + addPostData(text); + ignoredHeaders.add("content-length"); + } else if ( + data.postDataText && + (utils.isUrlEncodedRequest(data) || + ["PUT", "POST", "PATCH"].includes(data.method)) + ) { + // When no postData exists, --data-raw should not be set + postDataText = data.postDataText; + addPostData("--data-raw"); + addPostData(utils.writePostDataTextParams(postDataText)); + ignoredHeaders.add("content-length"); + } + // curl generates the host header itself based on the given URL + ignoredHeaders.add("host"); + + // Add --compressed if the response is compressed + if (utils.isContentEncodedResponse(data)) { + addParam("--compressed"); + } + + // Add -I (HEAD) + // For servers that supports HEAD. + // This will fetch the header of a document only. + if (data.method === "HEAD") { + addParam("-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); + } + + // Add request headers. + let headers = data.headers; + if (multipartRequest) { + const multipartHeaders = utils.getHeadersFromMultipartText(postDataText); + headers = headers.concat(multipartHeaders); + } + for (let i = 0; i < headers.length; i++) { + const header = headers[i]; + if (ignoredHeaders.has(header.name.toLowerCase())) { + continue; + } + addParam("-H"); + addParam(header.name + ": " + header.value); + } + + // Add post data. + command = command.concat(postData); + + return command.join(" "); + }, +}; + +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(data) { + let postDataText = data.postDataText; + if (!postDataText) { + return false; + } + + postDataText = postDataText.toLowerCase(); + if ( + postDataText.includes("content-type: application/x-www-form-urlencoded") + ) { + return true; + } + + const 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(data) { + let postDataText = data.postDataText; + if (!postDataText) { + return false; + } + + postDataText = postDataText.toLowerCase(); + if (postDataText.includes("content-type: multipart/form-data")) { + return true; + } + + const contentType = this.findHeader(data.headers, "content-type"); + + return ( + contentType && contentType.toLowerCase().includes("multipart/form-data;") + ); + }, + + /** + * Check if the response of an URL has content encoding header. + * + * @param object data + * The data source. See the description in the Curl object. + * @return boolean + * True if the response is compressed, false otherwise. + */ + isContentEncodedResponse(data) { + return !!this.findHeader(data.responseHeaders, "content-encoding"); + }, + + /** + * Write out paramters from post data text. + * + * @param object postDataText + * Post data text. + * @return string + * Post data parameters. + */ + writePostDataTextParams(postDataText) { + if (!postDataText) { + return ""; + } + const 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(headers, name) { + if (!headers) { + return null; + } + + name = name.toLowerCase(); + for (const 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(data) { + const boundaryRe = /\bboundary=(-{3,}\w+)/i; + + // Get the boundary string from the Content-Type request header. + const 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. + const 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(multipartText, boundary) { + let result = ""; + boundary = "--" + boundary; + const parts = multipartText.split(boundary); + for (const 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. + const headers = part.split("\r\n\r\n")[0]; + result += boundary + headers + "\r\n\r\n"; + } else { + result += boundary + 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(multipartText) { + const headers = []; + if (!multipartText || multipartText.startsWith("---")) { + return headers; + } + + // Get the header section. + const index = multipartText.indexOf("\r\n\r\n"); + if (index == -1) { + return headers; + } + + // Parse the header lines. + const headersText = multipartText.substring(0, index); + const headerLines = headersText.split("\r\n"); + let lastHeaderName = null; + + for (const 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; + } + + const indexOfColon = line.indexOf(":"); + if (indexOfColon == -1) { + continue; + } + + const 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(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. + * Credit: Google DevTools + */ + escapeStringWin(str) { + /* + Because cmd.exe parser and MS Crt arguments parsers use some of the + same escape characters, they can interact with each other in + horrible ways, the order of operations is critical. + */ + 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 MS Crt parser safely. + .replace(/[^a-zA-Z0-9\s_\-:=+~\/.',?;()*\$&\\{}\"`]/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, "%^") + + // 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"') + + encapsChars + ); + }, +}; + +exports.CurlUtils = CurlUtils; |