michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /* michael@0: * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. michael@0: * Copyright (C) 2008, 2009 Anthony Ricaud michael@0: * Copyright (C) 2011 Google Inc. All rights reserved. michael@0: * Copyright (C) 2009 Mozilla Foundation. All rights reserved. michael@0: * michael@0: * Redistribution and use in source and binary forms, with or without michael@0: * modification, are permitted provided that the following conditions michael@0: * are met: michael@0: * michael@0: * 1. Redistributions of source code must retain the above copyright michael@0: * notice, this list of conditions and the following disclaimer. michael@0: * 2. Redistributions in binary form must reproduce the above copyright michael@0: * notice, this list of conditions and the following disclaimer in the michael@0: * documentation and/or other materials provided with the distribution. michael@0: * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of michael@0: * its contributors may be used to endorse or promote products derived michael@0: * from this software without specific prior written permission. michael@0: * michael@0: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" michael@0: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE michael@0: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE michael@0: * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY michael@0: * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES michael@0: * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; michael@0: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND michael@0: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT michael@0: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF michael@0: * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Curl", "CurlUtils"]; michael@0: michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const DEFAULT_HTTP_VERSION = "HTTP/1.1"; michael@0: michael@0: this.Curl = { michael@0: /** michael@0: * Generates a cURL command string which can be used from the command line etc. michael@0: * michael@0: * @param object aData michael@0: * Datasource to create the command from. michael@0: * The object must contain the following properties: michael@0: * - url:string, the URL of the request. michael@0: * - method:string, the request method upper cased. HEAD / GET / POST etc. michael@0: * - headers:array, an array of request headers {name:x, value:x} tuples. michael@0: * - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1" michael@0: * - postDataText:string, optional - the request payload. michael@0: * michael@0: * @return string michael@0: * A cURL command. michael@0: */ michael@0: generateCommand: function(aData) { michael@0: let utils = CurlUtils; michael@0: michael@0: let command = ["curl"]; michael@0: let ignoredHeaders = new Set(); michael@0: michael@0: // The cURL command is expected to run on the same platform that Firefox runs michael@0: // (it may be different from the inspected page platform). michael@0: let escapeString = Services.appinfo.OS == "WINNT" ? michael@0: utils.escapeStringWin : utils.escapeStringPosix; michael@0: michael@0: // Add URL. michael@0: command.push(escapeString(aData.url)); michael@0: michael@0: let postDataText = null; michael@0: let multipartRequest = utils.isMultipartRequest(aData); michael@0: michael@0: // Create post data. michael@0: let data = []; michael@0: if (utils.isUrlEncodedRequest(aData) || aData.method == "PUT") { michael@0: postDataText = aData.postDataText; michael@0: data.push("--data"); michael@0: data.push(escapeString(utils.writePostDataTextParams(postDataText))); michael@0: ignoredHeaders.add("Content-Length"); michael@0: } else if (multipartRequest) { michael@0: postDataText = aData.postDataText; michael@0: data.push("--data-binary"); michael@0: let boundary = utils.getMultipartBoundary(aData); michael@0: let text = utils.removeBinaryDataFromMultipartText(postDataText, boundary); michael@0: data.push(escapeString(text)); michael@0: ignoredHeaders.add("Content-Length"); michael@0: } michael@0: michael@0: // Add method. michael@0: // For GET and POST requests this is not necessary as GET is the michael@0: // default. If --data or --binary is added POST is the default. michael@0: if (!(aData.method == "GET" || aData.method == "POST")) { michael@0: command.push("-X"); michael@0: command.push(aData.method); michael@0: } michael@0: michael@0: // Add -I (HEAD) michael@0: // For servers that supports HEAD. michael@0: // This will fetch the header of a document only. michael@0: if (aData.method == "HEAD") { michael@0: command.push("-I"); michael@0: } michael@0: michael@0: // Add http version. michael@0: if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) { michael@0: command.push("--" + aData.httpVersion.split("/")[1]); michael@0: } michael@0: michael@0: // Add request headers. michael@0: let headers = aData.headers; michael@0: if (multipartRequest) { michael@0: let multipartHeaders = utils.getHeadersFromMultipartText(postDataText); michael@0: headers = headers.concat(multipartHeaders); michael@0: } michael@0: for (let i = 0; i < headers.length; i++) { michael@0: let header = headers[i]; michael@0: if (ignoredHeaders.has(header.name)) { michael@0: continue; michael@0: } michael@0: command.push("-H"); michael@0: command.push(escapeString(header.name + ": " + header.value)); michael@0: } michael@0: michael@0: // Add post data. michael@0: command = command.concat(data); michael@0: michael@0: return command.join(" "); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Utility functions for the Curl command generator. michael@0: */ michael@0: this.CurlUtils = { michael@0: /** michael@0: * Check if the request is an URL encoded request. michael@0: * michael@0: * @param object aData michael@0: * The data source. See the description in the Curl object. michael@0: * @return boolean michael@0: * True if the request is URL encoded, false otherwise. michael@0: */ michael@0: isUrlEncodedRequest: function(aData) { michael@0: let postDataText = aData.postDataText; michael@0: if (!postDataText) { michael@0: return false; michael@0: } michael@0: michael@0: postDataText = postDataText.toLowerCase(); michael@0: if (postDataText.contains("content-type: application/x-www-form-urlencoded")) { michael@0: return true; michael@0: } michael@0: michael@0: let contentType = this.findHeader(aData.headers, "content-type"); michael@0: michael@0: return (contentType && michael@0: contentType.toLowerCase().contains("application/x-www-form-urlencoded")); michael@0: }, michael@0: michael@0: /** michael@0: * Check if the request is a multipart request. michael@0: * michael@0: * @param object aData michael@0: * The data source. michael@0: * @return boolean michael@0: * True if the request is multipart reqeust, false otherwise. michael@0: */ michael@0: isMultipartRequest: function(aData) { michael@0: let postDataText = aData.postDataText; michael@0: if (!postDataText) { michael@0: return false; michael@0: } michael@0: michael@0: postDataText = postDataText.toLowerCase(); michael@0: if (postDataText.contains("content-type: multipart/form-data")) { michael@0: return true; michael@0: } michael@0: michael@0: let contentType = this.findHeader(aData.headers, "content-type"); michael@0: michael@0: return (contentType && michael@0: contentType.toLowerCase().contains("multipart/form-data;")); michael@0: }, michael@0: michael@0: /** michael@0: * Write out paramters from post data text. michael@0: * michael@0: * @param object aPostDataText michael@0: * Post data text. michael@0: * @return string michael@0: * Post data parameters. michael@0: */ michael@0: writePostDataTextParams: function(aPostDataText) { michael@0: let lines = aPostDataText.split("\r\n"); michael@0: return lines[lines.length - 1]; michael@0: }, michael@0: michael@0: /** michael@0: * Finds the header with the given name in the headers array. michael@0: * michael@0: * @param array aHeaders michael@0: * Array of headers info {name:x, value:x}. michael@0: * @param string aName michael@0: * The header name to find. michael@0: * @return string michael@0: * The found header value or null if not found. michael@0: */ michael@0: findHeader: function(aHeaders, aName) { michael@0: if (!aHeaders) { michael@0: return null; michael@0: } michael@0: michael@0: let name = aName.toLowerCase(); michael@0: for (let header of aHeaders) { michael@0: if (name == header.name.toLowerCase()) { michael@0: return header.value; michael@0: } michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the boundary string for a multipart request. michael@0: * michael@0: * @param string aData michael@0: * The data source. See the description in the Curl object. michael@0: * @return string michael@0: * The boundary string for the request. michael@0: */ michael@0: getMultipartBoundary: function(aData) { michael@0: let boundaryRe = /\bboundary=(-{3,}\w+)/i; michael@0: michael@0: // Get the boundary string from the Content-Type request header. michael@0: let contentType = this.findHeader(aData.headers, "Content-Type"); michael@0: if (boundaryRe.test(contentType)) { michael@0: return contentType.match(boundaryRe)[1]; michael@0: } michael@0: // Temporary workaround. As of 2014-03-11 the requestHeaders array does not michael@0: // always contain the Content-Type header for mulitpart requests. See bug 978144. michael@0: // Find the header from the request payload. michael@0: let boundaryString = aData.postDataText.match(boundaryRe)[1]; michael@0: if (boundaryString) { michael@0: return boundaryString; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Removes the binary data from mulitpart text. michael@0: * michael@0: * @param string aMultipartText michael@0: * Multipart form data text. michael@0: * @param string aBoundary michael@0: * The boundary string. michael@0: * @return string michael@0: * The mulitpart text without the binary data. michael@0: */ michael@0: removeBinaryDataFromMultipartText: function(aMultipartText, aBoundary) { michael@0: let result = ""; michael@0: let boundary = "--" + aBoundary; michael@0: let parts = aMultipartText.split(boundary); michael@0: for (let part of parts) { michael@0: // Each part is expected to have a content disposition line. michael@0: let contentDispositionLine = part.trimLeft().split("\r\n")[0]; michael@0: if (!contentDispositionLine) { michael@0: continue; michael@0: } michael@0: contentDispositionLine = contentDispositionLine.toLowerCase(); michael@0: if (contentDispositionLine.contains("content-disposition: form-data")) { michael@0: if (contentDispositionLine.contains("filename=")) { michael@0: // The header lines and the binary blob is separated by 2 CRLF's. michael@0: // Add only the headers to the result. michael@0: let headers = part.split("\r\n\r\n")[0]; michael@0: result += boundary + "\r\n" + headers + "\r\n\r\n"; michael@0: } michael@0: else { michael@0: result += boundary + "\r\n" + part; michael@0: } michael@0: } michael@0: } michael@0: result += aBoundary + "--\r\n"; michael@0: michael@0: return result; michael@0: }, michael@0: michael@0: /** michael@0: * Get the headers from a multipart post data text. michael@0: * michael@0: * @param string aMultipartText michael@0: * Multipart post text. michael@0: * @return array michael@0: * An array of header objects {name:x, value:x} michael@0: */ michael@0: getHeadersFromMultipartText: function(aMultipartText) { michael@0: let headers = []; michael@0: if (!aMultipartText || aMultipartText.startsWith("---")) { michael@0: return headers; michael@0: } michael@0: michael@0: // Get the header section. michael@0: let index = aMultipartText.indexOf("\r\n\r\n"); michael@0: if (index == -1) { michael@0: return headers; michael@0: } michael@0: michael@0: // Parse the header lines. michael@0: let headersText = aMultipartText.substring(0, index); michael@0: let headerLines = headersText.split("\r\n"); michael@0: let lastHeaderName = null; michael@0: michael@0: for (let line of headerLines) { michael@0: // Create a header for each line in fields that spans across multiple lines. michael@0: // Subsquent lines always begins with at least one space or tab character. michael@0: // (rfc2616) michael@0: if (lastHeaderName && /^\s+/.test(line)) { michael@0: headers.push({ name: lastHeaderName, value: line.trim() }); michael@0: continue; michael@0: } michael@0: michael@0: let indexOfColon = line.indexOf(":"); michael@0: if (indexOfColon == -1) { michael@0: continue; michael@0: } michael@0: michael@0: let header = [line.slice(0, indexOfColon), line.slice(indexOfColon + 1)]; michael@0: if (header.length != 2) { michael@0: continue; michael@0: } michael@0: lastHeaderName = header[0].trim(); michael@0: headers.push({ name: lastHeaderName, value: header[1].trim() }); michael@0: } michael@0: michael@0: return headers; michael@0: }, michael@0: michael@0: /** michael@0: * Escape util function for POSIX oriented operating systems. michael@0: * Credit: Google DevTools michael@0: */ michael@0: escapeStringPosix: function(str) { michael@0: function escapeCharacter(x) { michael@0: let code = x.charCodeAt(0); michael@0: if (code < 256) { michael@0: // Add leading zero when needed to not care about the next character. michael@0: return code < 16 ? "\\x0" + code.toString(16) : "\\x" + code.toString(16); michael@0: } michael@0: code = code.toString(16); michael@0: return "\\u" + ("0000" + code).substr(code.length, 4); michael@0: } michael@0: michael@0: if (/[^\x20-\x7E]|\'/.test(str)) { michael@0: // Use ANSI-C quoting syntax. michael@0: return "$\'" + str.replace(/\\/g, "\\\\") michael@0: .replace(/\'/g, "\\\'") michael@0: .replace(/\n/g, "\\n") michael@0: .replace(/\r/g, "\\r") michael@0: .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'"; michael@0: } else { michael@0: // Use single quote syntax. michael@0: return "'" + str + "'"; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Escape util function for Windows systems. michael@0: * Credit: Google DevTools michael@0: */ michael@0: escapeStringWin: function(str) { michael@0: /* Replace quote by double quote (but not by \") because it is michael@0: recognized by both cmd.exe and MS Crt arguments parser. michael@0: michael@0: Replace % by "%" because it could be expanded to an environment michael@0: variable value. So %% becomes "%""%". Even if an env variable "" michael@0: (2 doublequotes) is declared, the cmd.exe will not michael@0: substitute it with its value. michael@0: michael@0: Replace each backslash with double backslash to make sure michael@0: MS Crt arguments parser won't collapse them. michael@0: michael@0: Replace new line outside of quotes since cmd.exe doesn't let michael@0: to do it inside. michael@0: */ michael@0: return "\"" + str.replace(/"/g, "\"\"") michael@0: .replace(/%/g, "\"%\"") michael@0: .replace(/\\/g, "\\\\") michael@0: .replace(/[\r\n]+/g, "\"^$&\"") + "\""; michael@0: } michael@0: };