browser/devtools/shared/Curl.jsm

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

mercurial