1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/shared/Curl.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,396 @@ 1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +/* 1.11 + * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 1.12 + * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org> 1.13 + * Copyright (C) 2011 Google Inc. All rights reserved. 1.14 + * Copyright (C) 2009 Mozilla Foundation. All rights reserved. 1.15 + * 1.16 + * Redistribution and use in source and binary forms, with or without 1.17 + * modification, are permitted provided that the following conditions 1.18 + * are met: 1.19 + * 1.20 + * 1. Redistributions of source code must retain the above copyright 1.21 + * notice, this list of conditions and the following disclaimer. 1.22 + * 2. Redistributions in binary form must reproduce the above copyright 1.23 + * notice, this list of conditions and the following disclaimer in the 1.24 + * documentation and/or other materials provided with the distribution. 1.25 + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 1.26 + * its contributors may be used to endorse or promote products derived 1.27 + * from this software without specific prior written permission. 1.28 + * 1.29 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 1.30 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 1.31 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 1.32 + * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 1.33 + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 1.34 + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 1.35 + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 1.36 + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 1.37 + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 1.38 + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 1.39 + */ 1.40 + 1.41 +"use strict"; 1.42 + 1.43 +this.EXPORTED_SYMBOLS = ["Curl", "CurlUtils"]; 1.44 + 1.45 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.46 + 1.47 +const DEFAULT_HTTP_VERSION = "HTTP/1.1"; 1.48 + 1.49 +this.Curl = { 1.50 + /** 1.51 + * Generates a cURL command string which can be used from the command line etc. 1.52 + * 1.53 + * @param object aData 1.54 + * Datasource to create the command from. 1.55 + * The object must contain the following properties: 1.56 + * - url:string, the URL of the request. 1.57 + * - method:string, the request method upper cased. HEAD / GET / POST etc. 1.58 + * - headers:array, an array of request headers {name:x, value:x} tuples. 1.59 + * - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1" 1.60 + * - postDataText:string, optional - the request payload. 1.61 + * 1.62 + * @return string 1.63 + * A cURL command. 1.64 + */ 1.65 + generateCommand: function(aData) { 1.66 + let utils = CurlUtils; 1.67 + 1.68 + let command = ["curl"]; 1.69 + let ignoredHeaders = new Set(); 1.70 + 1.71 + // The cURL command is expected to run on the same platform that Firefox runs 1.72 + // (it may be different from the inspected page platform). 1.73 + let escapeString = Services.appinfo.OS == "WINNT" ? 1.74 + utils.escapeStringWin : utils.escapeStringPosix; 1.75 + 1.76 + // Add URL. 1.77 + command.push(escapeString(aData.url)); 1.78 + 1.79 + let postDataText = null; 1.80 + let multipartRequest = utils.isMultipartRequest(aData); 1.81 + 1.82 + // Create post data. 1.83 + let data = []; 1.84 + if (utils.isUrlEncodedRequest(aData) || aData.method == "PUT") { 1.85 + postDataText = aData.postDataText; 1.86 + data.push("--data"); 1.87 + data.push(escapeString(utils.writePostDataTextParams(postDataText))); 1.88 + ignoredHeaders.add("Content-Length"); 1.89 + } else if (multipartRequest) { 1.90 + postDataText = aData.postDataText; 1.91 + data.push("--data-binary"); 1.92 + let boundary = utils.getMultipartBoundary(aData); 1.93 + let text = utils.removeBinaryDataFromMultipartText(postDataText, boundary); 1.94 + data.push(escapeString(text)); 1.95 + ignoredHeaders.add("Content-Length"); 1.96 + } 1.97 + 1.98 + // Add method. 1.99 + // For GET and POST requests this is not necessary as GET is the 1.100 + // default. If --data or --binary is added POST is the default. 1.101 + if (!(aData.method == "GET" || aData.method == "POST")) { 1.102 + command.push("-X"); 1.103 + command.push(aData.method); 1.104 + } 1.105 + 1.106 + // Add -I (HEAD) 1.107 + // For servers that supports HEAD. 1.108 + // This will fetch the header of a document only. 1.109 + if (aData.method == "HEAD") { 1.110 + command.push("-I"); 1.111 + } 1.112 + 1.113 + // Add http version. 1.114 + if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) { 1.115 + command.push("--" + aData.httpVersion.split("/")[1]); 1.116 + } 1.117 + 1.118 + // Add request headers. 1.119 + let headers = aData.headers; 1.120 + if (multipartRequest) { 1.121 + let multipartHeaders = utils.getHeadersFromMultipartText(postDataText); 1.122 + headers = headers.concat(multipartHeaders); 1.123 + } 1.124 + for (let i = 0; i < headers.length; i++) { 1.125 + let header = headers[i]; 1.126 + if (ignoredHeaders.has(header.name)) { 1.127 + continue; 1.128 + } 1.129 + command.push("-H"); 1.130 + command.push(escapeString(header.name + ": " + header.value)); 1.131 + } 1.132 + 1.133 + // Add post data. 1.134 + command = command.concat(data); 1.135 + 1.136 + return command.join(" "); 1.137 + } 1.138 +}; 1.139 + 1.140 +/** 1.141 + * Utility functions for the Curl command generator. 1.142 + */ 1.143 +this.CurlUtils = { 1.144 + /** 1.145 + * Check if the request is an URL encoded request. 1.146 + * 1.147 + * @param object aData 1.148 + * The data source. See the description in the Curl object. 1.149 + * @return boolean 1.150 + * True if the request is URL encoded, false otherwise. 1.151 + */ 1.152 + isUrlEncodedRequest: function(aData) { 1.153 + let postDataText = aData.postDataText; 1.154 + if (!postDataText) { 1.155 + return false; 1.156 + } 1.157 + 1.158 + postDataText = postDataText.toLowerCase(); 1.159 + if (postDataText.contains("content-type: application/x-www-form-urlencoded")) { 1.160 + return true; 1.161 + } 1.162 + 1.163 + let contentType = this.findHeader(aData.headers, "content-type"); 1.164 + 1.165 + return (contentType && 1.166 + contentType.toLowerCase().contains("application/x-www-form-urlencoded")); 1.167 + }, 1.168 + 1.169 + /** 1.170 + * Check if the request is a multipart request. 1.171 + * 1.172 + * @param object aData 1.173 + * The data source. 1.174 + * @return boolean 1.175 + * True if the request is multipart reqeust, false otherwise. 1.176 + */ 1.177 + isMultipartRequest: function(aData) { 1.178 + let postDataText = aData.postDataText; 1.179 + if (!postDataText) { 1.180 + return false; 1.181 + } 1.182 + 1.183 + postDataText = postDataText.toLowerCase(); 1.184 + if (postDataText.contains("content-type: multipart/form-data")) { 1.185 + return true; 1.186 + } 1.187 + 1.188 + let contentType = this.findHeader(aData.headers, "content-type"); 1.189 + 1.190 + return (contentType && 1.191 + contentType.toLowerCase().contains("multipart/form-data;")); 1.192 + }, 1.193 + 1.194 + /** 1.195 + * Write out paramters from post data text. 1.196 + * 1.197 + * @param object aPostDataText 1.198 + * Post data text. 1.199 + * @return string 1.200 + * Post data parameters. 1.201 + */ 1.202 + writePostDataTextParams: function(aPostDataText) { 1.203 + let lines = aPostDataText.split("\r\n"); 1.204 + return lines[lines.length - 1]; 1.205 + }, 1.206 + 1.207 + /** 1.208 + * Finds the header with the given name in the headers array. 1.209 + * 1.210 + * @param array aHeaders 1.211 + * Array of headers info {name:x, value:x}. 1.212 + * @param string aName 1.213 + * The header name to find. 1.214 + * @return string 1.215 + * The found header value or null if not found. 1.216 + */ 1.217 + findHeader: function(aHeaders, aName) { 1.218 + if (!aHeaders) { 1.219 + return null; 1.220 + } 1.221 + 1.222 + let name = aName.toLowerCase(); 1.223 + for (let header of aHeaders) { 1.224 + if (name == header.name.toLowerCase()) { 1.225 + return header.value; 1.226 + } 1.227 + } 1.228 + 1.229 + return null; 1.230 + }, 1.231 + 1.232 + /** 1.233 + * Returns the boundary string for a multipart request. 1.234 + * 1.235 + * @param string aData 1.236 + * The data source. See the description in the Curl object. 1.237 + * @return string 1.238 + * The boundary string for the request. 1.239 + */ 1.240 + getMultipartBoundary: function(aData) { 1.241 + let boundaryRe = /\bboundary=(-{3,}\w+)/i; 1.242 + 1.243 + // Get the boundary string from the Content-Type request header. 1.244 + let contentType = this.findHeader(aData.headers, "Content-Type"); 1.245 + if (boundaryRe.test(contentType)) { 1.246 + return contentType.match(boundaryRe)[1]; 1.247 + } 1.248 + // Temporary workaround. As of 2014-03-11 the requestHeaders array does not 1.249 + // always contain the Content-Type header for mulitpart requests. See bug 978144. 1.250 + // Find the header from the request payload. 1.251 + let boundaryString = aData.postDataText.match(boundaryRe)[1]; 1.252 + if (boundaryString) { 1.253 + return boundaryString; 1.254 + } 1.255 + 1.256 + return null; 1.257 + }, 1.258 + 1.259 + /** 1.260 + * Removes the binary data from mulitpart text. 1.261 + * 1.262 + * @param string aMultipartText 1.263 + * Multipart form data text. 1.264 + * @param string aBoundary 1.265 + * The boundary string. 1.266 + * @return string 1.267 + * The mulitpart text without the binary data. 1.268 + */ 1.269 + removeBinaryDataFromMultipartText: function(aMultipartText, aBoundary) { 1.270 + let result = ""; 1.271 + let boundary = "--" + aBoundary; 1.272 + let parts = aMultipartText.split(boundary); 1.273 + for (let part of parts) { 1.274 + // Each part is expected to have a content disposition line. 1.275 + let contentDispositionLine = part.trimLeft().split("\r\n")[0]; 1.276 + if (!contentDispositionLine) { 1.277 + continue; 1.278 + } 1.279 + contentDispositionLine = contentDispositionLine.toLowerCase(); 1.280 + if (contentDispositionLine.contains("content-disposition: form-data")) { 1.281 + if (contentDispositionLine.contains("filename=")) { 1.282 + // The header lines and the binary blob is separated by 2 CRLF's. 1.283 + // Add only the headers to the result. 1.284 + let headers = part.split("\r\n\r\n")[0]; 1.285 + result += boundary + "\r\n" + headers + "\r\n\r\n"; 1.286 + } 1.287 + else { 1.288 + result += boundary + "\r\n" + part; 1.289 + } 1.290 + } 1.291 + } 1.292 + result += aBoundary + "--\r\n"; 1.293 + 1.294 + return result; 1.295 + }, 1.296 + 1.297 + /** 1.298 + * Get the headers from a multipart post data text. 1.299 + * 1.300 + * @param string aMultipartText 1.301 + * Multipart post text. 1.302 + * @return array 1.303 + * An array of header objects {name:x, value:x} 1.304 + */ 1.305 + getHeadersFromMultipartText: function(aMultipartText) { 1.306 + let headers = []; 1.307 + if (!aMultipartText || aMultipartText.startsWith("---")) { 1.308 + return headers; 1.309 + } 1.310 + 1.311 + // Get the header section. 1.312 + let index = aMultipartText.indexOf("\r\n\r\n"); 1.313 + if (index == -1) { 1.314 + return headers; 1.315 + } 1.316 + 1.317 + // Parse the header lines. 1.318 + let headersText = aMultipartText.substring(0, index); 1.319 + let headerLines = headersText.split("\r\n"); 1.320 + let lastHeaderName = null; 1.321 + 1.322 + for (let line of headerLines) { 1.323 + // Create a header for each line in fields that spans across multiple lines. 1.324 + // Subsquent lines always begins with at least one space or tab character. 1.325 + // (rfc2616) 1.326 + if (lastHeaderName && /^\s+/.test(line)) { 1.327 + headers.push({ name: lastHeaderName, value: line.trim() }); 1.328 + continue; 1.329 + } 1.330 + 1.331 + let indexOfColon = line.indexOf(":"); 1.332 + if (indexOfColon == -1) { 1.333 + continue; 1.334 + } 1.335 + 1.336 + let header = [line.slice(0, indexOfColon), line.slice(indexOfColon + 1)]; 1.337 + if (header.length != 2) { 1.338 + continue; 1.339 + } 1.340 + lastHeaderName = header[0].trim(); 1.341 + headers.push({ name: lastHeaderName, value: header[1].trim() }); 1.342 + } 1.343 + 1.344 + return headers; 1.345 + }, 1.346 + 1.347 + /** 1.348 + * Escape util function for POSIX oriented operating systems. 1.349 + * Credit: Google DevTools 1.350 + */ 1.351 + escapeStringPosix: function(str) { 1.352 + function escapeCharacter(x) { 1.353 + let code = x.charCodeAt(0); 1.354 + if (code < 256) { 1.355 + // Add leading zero when needed to not care about the next character. 1.356 + return code < 16 ? "\\x0" + code.toString(16) : "\\x" + code.toString(16); 1.357 + } 1.358 + code = code.toString(16); 1.359 + return "\\u" + ("0000" + code).substr(code.length, 4); 1.360 + } 1.361 + 1.362 + if (/[^\x20-\x7E]|\'/.test(str)) { 1.363 + // Use ANSI-C quoting syntax. 1.364 + return "$\'" + str.replace(/\\/g, "\\\\") 1.365 + .replace(/\'/g, "\\\'") 1.366 + .replace(/\n/g, "\\n") 1.367 + .replace(/\r/g, "\\r") 1.368 + .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'"; 1.369 + } else { 1.370 + // Use single quote syntax. 1.371 + return "'" + str + "'"; 1.372 + } 1.373 + }, 1.374 + 1.375 + /** 1.376 + * Escape util function for Windows systems. 1.377 + * Credit: Google DevTools 1.378 + */ 1.379 + escapeStringWin: function(str) { 1.380 + /* Replace quote by double quote (but not by \") because it is 1.381 + recognized by both cmd.exe and MS Crt arguments parser. 1.382 + 1.383 + Replace % by "%" because it could be expanded to an environment 1.384 + variable value. So %% becomes "%""%". Even if an env variable "" 1.385 + (2 doublequotes) is declared, the cmd.exe will not 1.386 + substitute it with its value. 1.387 + 1.388 + Replace each backslash with double backslash to make sure 1.389 + MS Crt arguments parser won't collapse them. 1.390 + 1.391 + Replace new line outside of quotes since cmd.exe doesn't let 1.392 + to do it inside. 1.393 + */ 1.394 + return "\"" + str.replace(/"/g, "\"\"") 1.395 + .replace(/%/g, "\"%\"") 1.396 + .replace(/\\/g, "\\\\") 1.397 + .replace(/[\r\n]+/g, "\"^$&\"") + "\""; 1.398 + } 1.399 +};