Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
michael@0 | 3 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 6 | |
michael@0 | 7 | /* |
michael@0 | 8 | * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. |
michael@0 | 9 | * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org> |
michael@0 | 10 | * Copyright (C) 2011 Google Inc. All rights reserved. |
michael@0 | 11 | * Copyright (C) 2009 Mozilla Foundation. All rights reserved. |
michael@0 | 12 | * |
michael@0 | 13 | * Redistribution and use in source and binary forms, with or without |
michael@0 | 14 | * modification, are permitted provided that the following conditions |
michael@0 | 15 | * are met: |
michael@0 | 16 | * |
michael@0 | 17 | * 1. Redistributions of source code must retain the above copyright |
michael@0 | 18 | * notice, this list of conditions and the following disclaimer. |
michael@0 | 19 | * 2. Redistributions in binary form must reproduce the above copyright |
michael@0 | 20 | * notice, this list of conditions and the following disclaimer in the |
michael@0 | 21 | * documentation and/or other materials provided with the distribution. |
michael@0 | 22 | * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
michael@0 | 23 | * its contributors may be used to endorse or promote products derived |
michael@0 | 24 | * from this software without specific prior written permission. |
michael@0 | 25 | * |
michael@0 | 26 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
michael@0 | 27 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
michael@0 | 28 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
michael@0 | 29 | * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
michael@0 | 30 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
michael@0 | 31 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
michael@0 | 32 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
michael@0 | 33 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
michael@0 | 34 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
michael@0 | 35 | * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
michael@0 | 36 | */ |
michael@0 | 37 | |
michael@0 | 38 | "use strict"; |
michael@0 | 39 | |
michael@0 | 40 | this.EXPORTED_SYMBOLS = ["Curl", "CurlUtils"]; |
michael@0 | 41 | |
michael@0 | 42 | Components.utils.import("resource://gre/modules/Services.jsm"); |
michael@0 | 43 | |
michael@0 | 44 | const DEFAULT_HTTP_VERSION = "HTTP/1.1"; |
michael@0 | 45 | |
michael@0 | 46 | this.Curl = { |
michael@0 | 47 | /** |
michael@0 | 48 | * Generates a cURL command string which can be used from the command line etc. |
michael@0 | 49 | * |
michael@0 | 50 | * @param object aData |
michael@0 | 51 | * Datasource to create the command from. |
michael@0 | 52 | * The object must contain the following properties: |
michael@0 | 53 | * - url:string, the URL of the request. |
michael@0 | 54 | * - method:string, the request method upper cased. HEAD / GET / POST etc. |
michael@0 | 55 | * - headers:array, an array of request headers {name:x, value:x} tuples. |
michael@0 | 56 | * - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1" |
michael@0 | 57 | * - postDataText:string, optional - the request payload. |
michael@0 | 58 | * |
michael@0 | 59 | * @return string |
michael@0 | 60 | * A cURL command. |
michael@0 | 61 | */ |
michael@0 | 62 | generateCommand: function(aData) { |
michael@0 | 63 | let utils = CurlUtils; |
michael@0 | 64 | |
michael@0 | 65 | let command = ["curl"]; |
michael@0 | 66 | let ignoredHeaders = new Set(); |
michael@0 | 67 | |
michael@0 | 68 | // The cURL command is expected to run on the same platform that Firefox runs |
michael@0 | 69 | // (it may be different from the inspected page platform). |
michael@0 | 70 | let escapeString = Services.appinfo.OS == "WINNT" ? |
michael@0 | 71 | utils.escapeStringWin : utils.escapeStringPosix; |
michael@0 | 72 | |
michael@0 | 73 | // Add URL. |
michael@0 | 74 | command.push(escapeString(aData.url)); |
michael@0 | 75 | |
michael@0 | 76 | let postDataText = null; |
michael@0 | 77 | let multipartRequest = utils.isMultipartRequest(aData); |
michael@0 | 78 | |
michael@0 | 79 | // Create post data. |
michael@0 | 80 | let data = []; |
michael@0 | 81 | if (utils.isUrlEncodedRequest(aData) || aData.method == "PUT") { |
michael@0 | 82 | postDataText = aData.postDataText; |
michael@0 | 83 | data.push("--data"); |
michael@0 | 84 | data.push(escapeString(utils.writePostDataTextParams(postDataText))); |
michael@0 | 85 | ignoredHeaders.add("Content-Length"); |
michael@0 | 86 | } else if (multipartRequest) { |
michael@0 | 87 | postDataText = aData.postDataText; |
michael@0 | 88 | data.push("--data-binary"); |
michael@0 | 89 | let boundary = utils.getMultipartBoundary(aData); |
michael@0 | 90 | let text = utils.removeBinaryDataFromMultipartText(postDataText, boundary); |
michael@0 | 91 | data.push(escapeString(text)); |
michael@0 | 92 | ignoredHeaders.add("Content-Length"); |
michael@0 | 93 | } |
michael@0 | 94 | |
michael@0 | 95 | // Add method. |
michael@0 | 96 | // For GET and POST requests this is not necessary as GET is the |
michael@0 | 97 | // default. If --data or --binary is added POST is the default. |
michael@0 | 98 | if (!(aData.method == "GET" || aData.method == "POST")) { |
michael@0 | 99 | command.push("-X"); |
michael@0 | 100 | command.push(aData.method); |
michael@0 | 101 | } |
michael@0 | 102 | |
michael@0 | 103 | // Add -I (HEAD) |
michael@0 | 104 | // For servers that supports HEAD. |
michael@0 | 105 | // This will fetch the header of a document only. |
michael@0 | 106 | if (aData.method == "HEAD") { |
michael@0 | 107 | command.push("-I"); |
michael@0 | 108 | } |
michael@0 | 109 | |
michael@0 | 110 | // Add http version. |
michael@0 | 111 | if (aData.httpVersion && aData.httpVersion != DEFAULT_HTTP_VERSION) { |
michael@0 | 112 | command.push("--" + aData.httpVersion.split("/")[1]); |
michael@0 | 113 | } |
michael@0 | 114 | |
michael@0 | 115 | // Add request headers. |
michael@0 | 116 | let headers = aData.headers; |
michael@0 | 117 | if (multipartRequest) { |
michael@0 | 118 | let multipartHeaders = utils.getHeadersFromMultipartText(postDataText); |
michael@0 | 119 | headers = headers.concat(multipartHeaders); |
michael@0 | 120 | } |
michael@0 | 121 | for (let i = 0; i < headers.length; i++) { |
michael@0 | 122 | let header = headers[i]; |
michael@0 | 123 | if (ignoredHeaders.has(header.name)) { |
michael@0 | 124 | continue; |
michael@0 | 125 | } |
michael@0 | 126 | command.push("-H"); |
michael@0 | 127 | command.push(escapeString(header.name + ": " + header.value)); |
michael@0 | 128 | } |
michael@0 | 129 | |
michael@0 | 130 | // Add post data. |
michael@0 | 131 | command = command.concat(data); |
michael@0 | 132 | |
michael@0 | 133 | return command.join(" "); |
michael@0 | 134 | } |
michael@0 | 135 | }; |
michael@0 | 136 | |
michael@0 | 137 | /** |
michael@0 | 138 | * Utility functions for the Curl command generator. |
michael@0 | 139 | */ |
michael@0 | 140 | this.CurlUtils = { |
michael@0 | 141 | /** |
michael@0 | 142 | * Check if the request is an URL encoded request. |
michael@0 | 143 | * |
michael@0 | 144 | * @param object aData |
michael@0 | 145 | * The data source. See the description in the Curl object. |
michael@0 | 146 | * @return boolean |
michael@0 | 147 | * True if the request is URL encoded, false otherwise. |
michael@0 | 148 | */ |
michael@0 | 149 | isUrlEncodedRequest: function(aData) { |
michael@0 | 150 | let postDataText = aData.postDataText; |
michael@0 | 151 | if (!postDataText) { |
michael@0 | 152 | return false; |
michael@0 | 153 | } |
michael@0 | 154 | |
michael@0 | 155 | postDataText = postDataText.toLowerCase(); |
michael@0 | 156 | if (postDataText.contains("content-type: application/x-www-form-urlencoded")) { |
michael@0 | 157 | return true; |
michael@0 | 158 | } |
michael@0 | 159 | |
michael@0 | 160 | let contentType = this.findHeader(aData.headers, "content-type"); |
michael@0 | 161 | |
michael@0 | 162 | return (contentType && |
michael@0 | 163 | contentType.toLowerCase().contains("application/x-www-form-urlencoded")); |
michael@0 | 164 | }, |
michael@0 | 165 | |
michael@0 | 166 | /** |
michael@0 | 167 | * Check if the request is a multipart request. |
michael@0 | 168 | * |
michael@0 | 169 | * @param object aData |
michael@0 | 170 | * The data source. |
michael@0 | 171 | * @return boolean |
michael@0 | 172 | * True if the request is multipart reqeust, false otherwise. |
michael@0 | 173 | */ |
michael@0 | 174 | isMultipartRequest: function(aData) { |
michael@0 | 175 | let postDataText = aData.postDataText; |
michael@0 | 176 | if (!postDataText) { |
michael@0 | 177 | return false; |
michael@0 | 178 | } |
michael@0 | 179 | |
michael@0 | 180 | postDataText = postDataText.toLowerCase(); |
michael@0 | 181 | if (postDataText.contains("content-type: multipart/form-data")) { |
michael@0 | 182 | return true; |
michael@0 | 183 | } |
michael@0 | 184 | |
michael@0 | 185 | let contentType = this.findHeader(aData.headers, "content-type"); |
michael@0 | 186 | |
michael@0 | 187 | return (contentType && |
michael@0 | 188 | contentType.toLowerCase().contains("multipart/form-data;")); |
michael@0 | 189 | }, |
michael@0 | 190 | |
michael@0 | 191 | /** |
michael@0 | 192 | * Write out paramters from post data text. |
michael@0 | 193 | * |
michael@0 | 194 | * @param object aPostDataText |
michael@0 | 195 | * Post data text. |
michael@0 | 196 | * @return string |
michael@0 | 197 | * Post data parameters. |
michael@0 | 198 | */ |
michael@0 | 199 | writePostDataTextParams: function(aPostDataText) { |
michael@0 | 200 | let lines = aPostDataText.split("\r\n"); |
michael@0 | 201 | return lines[lines.length - 1]; |
michael@0 | 202 | }, |
michael@0 | 203 | |
michael@0 | 204 | /** |
michael@0 | 205 | * Finds the header with the given name in the headers array. |
michael@0 | 206 | * |
michael@0 | 207 | * @param array aHeaders |
michael@0 | 208 | * Array of headers info {name:x, value:x}. |
michael@0 | 209 | * @param string aName |
michael@0 | 210 | * The header name to find. |
michael@0 | 211 | * @return string |
michael@0 | 212 | * The found header value or null if not found. |
michael@0 | 213 | */ |
michael@0 | 214 | findHeader: function(aHeaders, aName) { |
michael@0 | 215 | if (!aHeaders) { |
michael@0 | 216 | return null; |
michael@0 | 217 | } |
michael@0 | 218 | |
michael@0 | 219 | let name = aName.toLowerCase(); |
michael@0 | 220 | for (let header of aHeaders) { |
michael@0 | 221 | if (name == header.name.toLowerCase()) { |
michael@0 | 222 | return header.value; |
michael@0 | 223 | } |
michael@0 | 224 | } |
michael@0 | 225 | |
michael@0 | 226 | return null; |
michael@0 | 227 | }, |
michael@0 | 228 | |
michael@0 | 229 | /** |
michael@0 | 230 | * Returns the boundary string for a multipart request. |
michael@0 | 231 | * |
michael@0 | 232 | * @param string aData |
michael@0 | 233 | * The data source. See the description in the Curl object. |
michael@0 | 234 | * @return string |
michael@0 | 235 | * The boundary string for the request. |
michael@0 | 236 | */ |
michael@0 | 237 | getMultipartBoundary: function(aData) { |
michael@0 | 238 | let boundaryRe = /\bboundary=(-{3,}\w+)/i; |
michael@0 | 239 | |
michael@0 | 240 | // Get the boundary string from the Content-Type request header. |
michael@0 | 241 | let contentType = this.findHeader(aData.headers, "Content-Type"); |
michael@0 | 242 | if (boundaryRe.test(contentType)) { |
michael@0 | 243 | return contentType.match(boundaryRe)[1]; |
michael@0 | 244 | } |
michael@0 | 245 | // Temporary workaround. As of 2014-03-11 the requestHeaders array does not |
michael@0 | 246 | // always contain the Content-Type header for mulitpart requests. See bug 978144. |
michael@0 | 247 | // Find the header from the request payload. |
michael@0 | 248 | let boundaryString = aData.postDataText.match(boundaryRe)[1]; |
michael@0 | 249 | if (boundaryString) { |
michael@0 | 250 | return boundaryString; |
michael@0 | 251 | } |
michael@0 | 252 | |
michael@0 | 253 | return null; |
michael@0 | 254 | }, |
michael@0 | 255 | |
michael@0 | 256 | /** |
michael@0 | 257 | * Removes the binary data from mulitpart text. |
michael@0 | 258 | * |
michael@0 | 259 | * @param string aMultipartText |
michael@0 | 260 | * Multipart form data text. |
michael@0 | 261 | * @param string aBoundary |
michael@0 | 262 | * The boundary string. |
michael@0 | 263 | * @return string |
michael@0 | 264 | * The mulitpart text without the binary data. |
michael@0 | 265 | */ |
michael@0 | 266 | removeBinaryDataFromMultipartText: function(aMultipartText, aBoundary) { |
michael@0 | 267 | let result = ""; |
michael@0 | 268 | let boundary = "--" + aBoundary; |
michael@0 | 269 | let parts = aMultipartText.split(boundary); |
michael@0 | 270 | for (let part of parts) { |
michael@0 | 271 | // Each part is expected to have a content disposition line. |
michael@0 | 272 | let contentDispositionLine = part.trimLeft().split("\r\n")[0]; |
michael@0 | 273 | if (!contentDispositionLine) { |
michael@0 | 274 | continue; |
michael@0 | 275 | } |
michael@0 | 276 | contentDispositionLine = contentDispositionLine.toLowerCase(); |
michael@0 | 277 | if (contentDispositionLine.contains("content-disposition: form-data")) { |
michael@0 | 278 | if (contentDispositionLine.contains("filename=")) { |
michael@0 | 279 | // The header lines and the binary blob is separated by 2 CRLF's. |
michael@0 | 280 | // Add only the headers to the result. |
michael@0 | 281 | let headers = part.split("\r\n\r\n")[0]; |
michael@0 | 282 | result += boundary + "\r\n" + headers + "\r\n\r\n"; |
michael@0 | 283 | } |
michael@0 | 284 | else { |
michael@0 | 285 | result += boundary + "\r\n" + part; |
michael@0 | 286 | } |
michael@0 | 287 | } |
michael@0 | 288 | } |
michael@0 | 289 | result += aBoundary + "--\r\n"; |
michael@0 | 290 | |
michael@0 | 291 | return result; |
michael@0 | 292 | }, |
michael@0 | 293 | |
michael@0 | 294 | /** |
michael@0 | 295 | * Get the headers from a multipart post data text. |
michael@0 | 296 | * |
michael@0 | 297 | * @param string aMultipartText |
michael@0 | 298 | * Multipart post text. |
michael@0 | 299 | * @return array |
michael@0 | 300 | * An array of header objects {name:x, value:x} |
michael@0 | 301 | */ |
michael@0 | 302 | getHeadersFromMultipartText: function(aMultipartText) { |
michael@0 | 303 | let headers = []; |
michael@0 | 304 | if (!aMultipartText || aMultipartText.startsWith("---")) { |
michael@0 | 305 | return headers; |
michael@0 | 306 | } |
michael@0 | 307 | |
michael@0 | 308 | // Get the header section. |
michael@0 | 309 | let index = aMultipartText.indexOf("\r\n\r\n"); |
michael@0 | 310 | if (index == -1) { |
michael@0 | 311 | return headers; |
michael@0 | 312 | } |
michael@0 | 313 | |
michael@0 | 314 | // Parse the header lines. |
michael@0 | 315 | let headersText = aMultipartText.substring(0, index); |
michael@0 | 316 | let headerLines = headersText.split("\r\n"); |
michael@0 | 317 | let lastHeaderName = null; |
michael@0 | 318 | |
michael@0 | 319 | for (let line of headerLines) { |
michael@0 | 320 | // Create a header for each line in fields that spans across multiple lines. |
michael@0 | 321 | // Subsquent lines always begins with at least one space or tab character. |
michael@0 | 322 | // (rfc2616) |
michael@0 | 323 | if (lastHeaderName && /^\s+/.test(line)) { |
michael@0 | 324 | headers.push({ name: lastHeaderName, value: line.trim() }); |
michael@0 | 325 | continue; |
michael@0 | 326 | } |
michael@0 | 327 | |
michael@0 | 328 | let indexOfColon = line.indexOf(":"); |
michael@0 | 329 | if (indexOfColon == -1) { |
michael@0 | 330 | continue; |
michael@0 | 331 | } |
michael@0 | 332 | |
michael@0 | 333 | let header = [line.slice(0, indexOfColon), line.slice(indexOfColon + 1)]; |
michael@0 | 334 | if (header.length != 2) { |
michael@0 | 335 | continue; |
michael@0 | 336 | } |
michael@0 | 337 | lastHeaderName = header[0].trim(); |
michael@0 | 338 | headers.push({ name: lastHeaderName, value: header[1].trim() }); |
michael@0 | 339 | } |
michael@0 | 340 | |
michael@0 | 341 | return headers; |
michael@0 | 342 | }, |
michael@0 | 343 | |
michael@0 | 344 | /** |
michael@0 | 345 | * Escape util function for POSIX oriented operating systems. |
michael@0 | 346 | * Credit: Google DevTools |
michael@0 | 347 | */ |
michael@0 | 348 | escapeStringPosix: function(str) { |
michael@0 | 349 | function escapeCharacter(x) { |
michael@0 | 350 | let code = x.charCodeAt(0); |
michael@0 | 351 | if (code < 256) { |
michael@0 | 352 | // Add leading zero when needed to not care about the next character. |
michael@0 | 353 | return code < 16 ? "\\x0" + code.toString(16) : "\\x" + code.toString(16); |
michael@0 | 354 | } |
michael@0 | 355 | code = code.toString(16); |
michael@0 | 356 | return "\\u" + ("0000" + code).substr(code.length, 4); |
michael@0 | 357 | } |
michael@0 | 358 | |
michael@0 | 359 | if (/[^\x20-\x7E]|\'/.test(str)) { |
michael@0 | 360 | // Use ANSI-C quoting syntax. |
michael@0 | 361 | return "$\'" + str.replace(/\\/g, "\\\\") |
michael@0 | 362 | .replace(/\'/g, "\\\'") |
michael@0 | 363 | .replace(/\n/g, "\\n") |
michael@0 | 364 | .replace(/\r/g, "\\r") |
michael@0 | 365 | .replace(/[^\x20-\x7E]/g, escapeCharacter) + "'"; |
michael@0 | 366 | } else { |
michael@0 | 367 | // Use single quote syntax. |
michael@0 | 368 | return "'" + str + "'"; |
michael@0 | 369 | } |
michael@0 | 370 | }, |
michael@0 | 371 | |
michael@0 | 372 | /** |
michael@0 | 373 | * Escape util function for Windows systems. |
michael@0 | 374 | * Credit: Google DevTools |
michael@0 | 375 | */ |
michael@0 | 376 | escapeStringWin: function(str) { |
michael@0 | 377 | /* Replace quote by double quote (but not by \") because it is |
michael@0 | 378 | recognized by both cmd.exe and MS Crt arguments parser. |
michael@0 | 379 | |
michael@0 | 380 | Replace % by "%" because it could be expanded to an environment |
michael@0 | 381 | variable value. So %% becomes "%""%". Even if an env variable "" |
michael@0 | 382 | (2 doublequotes) is declared, the cmd.exe will not |
michael@0 | 383 | substitute it with its value. |
michael@0 | 384 | |
michael@0 | 385 | Replace each backslash with double backslash to make sure |
michael@0 | 386 | MS Crt arguments parser won't collapse them. |
michael@0 | 387 | |
michael@0 | 388 | Replace new line outside of quotes since cmd.exe doesn't let |
michael@0 | 389 | to do it inside. |
michael@0 | 390 | */ |
michael@0 | 391 | return "\"" + str.replace(/"/g, "\"\"") |
michael@0 | 392 | .replace(/%/g, "\"%\"") |
michael@0 | 393 | .replace(/\\/g, "\\\\") |
michael@0 | 394 | .replace(/[\r\n]+/g, "\"^$&\"") + "\""; |
michael@0 | 395 | } |
michael@0 | 396 | }; |