summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/curl.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/curl.js')
-rw-r--r--devtools/client/shared/curl.js489
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;