browser/devtools/shared/Curl.jsm

changeset 0
6474c204b198
     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 +};

mercurial