curl.js (16845B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* 6 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. 7 * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org> 8 * Copyright (C) 2011 Google Inc. All rights reserved. 9 * Copyright (C) 2009 Mozilla Foundation. All rights reserved. 10 * 11 * Redistribution and use in source and binary forms, with or without 12 * modification, are permitted provided that the following conditions 13 * are met: 14 * 15 * 1. Redistributions of source code must retain the above copyright 16 * notice, this list of conditions and the following disclaimer. 17 * 2. Redistributions in binary form must reproduce the above copyright 18 * notice, this list of conditions and the following disclaimer in the 19 * documentation and/or other materials provided with the distribution. 20 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 21 * its contributors may be used to endorse or promote products derived 22 * from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 27 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 28 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 31 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 33 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 */ 35 36 "use strict"; 37 38 const Curl = { 39 /** 40 * Generates a cURL command string which can be used from the command line etc. 41 * 42 * @param object data 43 * Datasource to create the command from. 44 * The object must contain the following properties: 45 * - url:string, the URL of the request. 46 * - method:string, the request method upper cased. HEAD / GET / POST etc. 47 * - headers:array, an array of request headers {name:x, value:x} tuples. 48 * - httpVersion:string, http protocol version rfc2616 formatted. Eg. "HTTP/1.1" 49 * - postDataText:string, optional - the request payload. 50 * 51 * @param string platform 52 * Optional parameter to override platform, 53 * Fallbacks to current platform if not defined. 54 * 55 * @return string 56 * A cURL command. 57 */ 58 generateCommand(data, platform) { 59 const utils = CurlUtils; 60 61 let commandParts = []; 62 63 // Make sure to use the following helpers to sanitize arguments before execution. 64 const escapeStringifNeeded = value => { 65 return /^[a-zA-Z-]+$/.test(value) ? value : escapeString(value); 66 }; 67 68 const ignoredHeaders = new Set(); 69 const currentPlatform = platform || Services.appinfo.OS; 70 71 // The cURL command is expected to run on the same platform that Firefox runs 72 // (it may be different from the inspected page platform). 73 const escapeString = 74 currentPlatform === "WINNT" 75 ? utils.escapeStringWin 76 : utils.escapeStringPosix; 77 78 // Add URL. 79 commandParts.push(escapeString(data.url)); 80 81 // Disable globbing if the URL contains brackets. 82 // cURL also globs braces but they are already percent-encoded. 83 if (data.url.includes("[") || data.url.includes("]")) { 84 commandParts.push("--globoff"); 85 } 86 87 let postDataText = null; 88 const multipartRequest = utils.isMultipartRequest(data); 89 90 // Create post data. 91 const postData = []; 92 if (multipartRequest) { 93 // WINDOWS KNOWN LIMITATIONS: Due to the specificity of running curl on 94 // cmd.exe even correctly escaped windows newline \r\n will be 95 // treated by curl as plain local newline. It corresponds in unix 96 // to single \n and that's what curl will send in payload. 97 // It may be particularly hurtful for multipart/form-data payloads 98 // which composed using \n only, not \r\n, may be not parsable for 99 // peers which split parts of multipart payload using \r\n. 100 postDataText = data.postDataText; 101 postData.push("--data-binary"); 102 const boundary = utils.getMultipartBoundary(data); 103 const text = utils.removeBinaryDataFromMultipartText( 104 postDataText, 105 boundary 106 ); 107 postData.push(escapeStringifNeeded(text)); 108 ignoredHeaders.add("content-length"); 109 } else if ( 110 data.postDataText && 111 (utils.isUrlEncodedRequest(data) || 112 ["PUT", "POST", "PATCH"].includes(data.method)) 113 ) { 114 // When no postData exists, --data-raw should not be set 115 postDataText = data.postDataText; 116 postData.push( 117 "--data-raw " + 118 escapeStringifNeeded(`${utils.writePostDataTextParams(postDataText)}`) 119 ); 120 ignoredHeaders.add("content-length"); 121 } 122 // curl generates the host header itself based on the given URL 123 ignoredHeaders.add("host"); 124 125 // Add --compressed if the response is compressed 126 if (utils.isContentEncodedResponse(data)) { 127 commandParts.push("--compressed"); 128 } 129 130 // Add -I (HEAD) 131 // For servers that supports HEAD. 132 // This will fetch the header of a document only. 133 if (data.method === "HEAD") { 134 commandParts.push("-I"); 135 } else if (data.method !== "GET") { 136 // Add method. 137 // For HEAD and GET requests this is not necessary. GET is the 138 // default, -I implies HEAD. 139 commandParts.push("-X " + escapeStringifNeeded(`${data.method}`)); 140 } 141 142 // Add request headers. 143 let headers = data.headers; 144 if (multipartRequest) { 145 const multipartHeaders = utils.getHeadersFromMultipartText(postDataText); 146 headers = headers.concat(multipartHeaders); 147 } 148 for (let i = 0; i < headers.length; i++) { 149 const header = headers[i]; 150 if (ignoredHeaders.has(header.name.toLowerCase())) { 151 continue; 152 } 153 commandParts.push( 154 "-H " + escapeStringifNeeded(`${header.name}: ${header.value}`) 155 ); 156 } 157 158 // Add post data. 159 commandParts = commandParts.concat(postData); 160 161 // Format with line breaks if the command has more than 2 parts 162 // e.g 163 // Command with 2 parts - curl https://foo.com 164 // Commands with more than 2 parts - 165 // curl https://foo.com 166 // -X POST 167 // -H "Accept : */*" 168 // -H "accept-language: en-US" 169 const joinStr = currentPlatform === "WINNT" ? " ^\n " : " \\\n "; 170 const CMD = currentPlatform == "WINNT" ? "curl.exe " : "curl "; 171 return CMD + commandParts.join(commandParts.length >= 3 ? joinStr : " "); 172 }, 173 }; 174 175 exports.Curl = Curl; 176 177 /** 178 * Utility functions for the Curl command generator. 179 */ 180 const CurlUtils = { 181 /** 182 * Check if the request is an URL encoded request. 183 * 184 * @param object data 185 * The data source. See the description in the Curl object. 186 * @return boolean 187 * True if the request is URL encoded, false otherwise. 188 */ 189 isUrlEncodedRequest(data) { 190 let postDataText = data.postDataText; 191 if (!postDataText) { 192 return false; 193 } 194 195 postDataText = postDataText.toLowerCase(); 196 if ( 197 postDataText.includes("content-type: application/x-www-form-urlencoded") 198 ) { 199 return true; 200 } 201 202 const contentType = this.findHeader(data.headers, "content-type"); 203 204 return ( 205 contentType && 206 contentType.toLowerCase().includes("application/x-www-form-urlencoded") 207 ); 208 }, 209 210 /** 211 * Check if the request is a multipart request. 212 * 213 * @param object data 214 * The data source. 215 * @return boolean 216 * True if the request is multipart reqeust, false otherwise. 217 */ 218 isMultipartRequest(data) { 219 let postDataText = data.postDataText; 220 if (!postDataText) { 221 return false; 222 } 223 224 postDataText = postDataText.toLowerCase(); 225 if (postDataText.includes("content-type: multipart/form-data")) { 226 return true; 227 } 228 229 const contentType = this.findHeader(data.headers, "content-type"); 230 231 return ( 232 contentType && contentType.toLowerCase().includes("multipart/form-data;") 233 ); 234 }, 235 236 /** 237 * Check if the response of an URL has content encoding header. 238 * 239 * @param object data 240 * The data source. See the description in the Curl object. 241 * @return boolean 242 * True if the response is compressed, false otherwise. 243 */ 244 isContentEncodedResponse(data) { 245 return !!this.findHeader(data.responseHeaders, "content-encoding"); 246 }, 247 248 /** 249 * Write out paramters from post data text. 250 * 251 * @param object postDataText 252 * Post data text. 253 * @return string 254 * Post data parameters. 255 */ 256 writePostDataTextParams(postDataText) { 257 if (!postDataText) { 258 return ""; 259 } 260 const lines = postDataText.split("\r\n"); 261 return lines[lines.length - 1]; 262 }, 263 264 /** 265 * Finds the header with the given name in the headers array. 266 * 267 * @param array headers 268 * Array of headers info {name:x, value:x}. 269 * @param string name 270 * The header name to find. 271 * @return string 272 * The found header value or null if not found. 273 */ 274 findHeader(headers, name) { 275 if (!headers) { 276 return null; 277 } 278 279 name = name.toLowerCase(); 280 for (const header of headers) { 281 if (name == header.name.toLowerCase()) { 282 return header.value; 283 } 284 } 285 286 return null; 287 }, 288 289 /** 290 * Returns the boundary string for a multipart request. 291 * 292 * @param string data 293 * The data source. See the description in the Curl object. 294 * @return string 295 * The boundary string for the request. 296 */ 297 getMultipartBoundary(data) { 298 const boundaryRe = /\bboundary=(-{3,}\w+)/i; 299 300 // Get the boundary string from the Content-Type request header. 301 const contentType = this.findHeader(data.headers, "Content-Type"); 302 if (boundaryRe.test(contentType)) { 303 return contentType.match(boundaryRe)[1]; 304 } 305 // Temporary workaround. As of 2014-03-11 the requestHeaders array does not 306 // always contain the Content-Type header for mulitpart requests. See bug 978144. 307 // Find the header from the request payload. 308 const boundaryString = data.postDataText.match(boundaryRe)[1]; 309 if (boundaryString) { 310 return boundaryString; 311 } 312 313 return null; 314 }, 315 316 /** 317 * Removes the binary data from multipart text. 318 * 319 * @param string multipartText 320 * Multipart form data text. 321 * @param string boundary 322 * The boundary string. 323 * @return string 324 * The multipart text without the binary data. 325 */ 326 removeBinaryDataFromMultipartText(multipartText, boundary) { 327 let result = ""; 328 boundary = "--" + boundary; 329 const parts = multipartText.split(boundary); 330 for (const part of parts) { 331 // Each part is expected to have a content disposition line. 332 let contentDispositionLine = part.trimLeft().split("\r\n")[0]; 333 if (!contentDispositionLine) { 334 continue; 335 } 336 contentDispositionLine = contentDispositionLine.toLowerCase(); 337 if (contentDispositionLine.includes("content-disposition: form-data")) { 338 if (contentDispositionLine.includes("filename=")) { 339 // The header lines and the binary blob is separated by 2 CRLF's. 340 // Add only the headers to the result. 341 const headers = part.split("\r\n\r\n")[0]; 342 result += boundary + headers + "\r\n\r\n"; 343 } else { 344 result += boundary + part; 345 } 346 } 347 } 348 result += boundary + "--\r\n"; 349 350 return result; 351 }, 352 353 /** 354 * Get the headers from a multipart post data text. 355 * 356 * @param string multipartText 357 * Multipart post text. 358 * @return array 359 * An array of header objects {name:x, value:x} 360 */ 361 getHeadersFromMultipartText(multipartText) { 362 const headers = []; 363 if (!multipartText || multipartText.startsWith("---")) { 364 return headers; 365 } 366 367 // Get the header section. 368 const index = multipartText.indexOf("\r\n\r\n"); 369 if (index == -1) { 370 return headers; 371 } 372 373 // Parse the header lines. 374 const headersText = multipartText.substring(0, index); 375 const headerLines = headersText.split("\r\n"); 376 let lastHeaderName = null; 377 378 for (const line of headerLines) { 379 // Create a header for each line in fields that spans across multiple lines. 380 // Subsquent lines always begins with at least one space or tab character. 381 // (rfc2616) 382 if (lastHeaderName && /^\s+/.test(line)) { 383 headers.push({ name: lastHeaderName, value: line.trim() }); 384 continue; 385 } 386 387 const indexOfColon = line.indexOf(":"); 388 if (indexOfColon == -1) { 389 continue; 390 } 391 392 const header = [ 393 line.slice(0, indexOfColon), 394 line.slice(indexOfColon + 1), 395 ]; 396 if (header.length != 2) { 397 continue; 398 } 399 lastHeaderName = header[0].trim(); 400 headers.push({ name: lastHeaderName, value: header[1].trim() }); 401 } 402 403 return headers; 404 }, 405 406 /** 407 * Escape util function for POSIX oriented operating systems. 408 * Credit: Google DevTools 409 */ 410 escapeStringPosix(str) { 411 function escapeCharacter(x) { 412 let code = x.charCodeAt(0); 413 if (code < 256) { 414 // Add leading zero when needed to not care about the next character. 415 return code < 16 416 ? "\\x0" + code.toString(16) 417 : "\\x" + code.toString(16); 418 } 419 code = code.toString(16); 420 return "\\u" + ("0000" + code).substr(code.length, 4); 421 } 422 423 // Escape characters which are not within the charater range 424 // SPACE to "~"(char codes 32 - 126), the `!` (code 33) and '(code 39); 425 if (/[^\x20-\x7E]|!|\'/.test(str)) { 426 // Use ANSI-C quoting syntax. 427 return ( 428 "$'" + 429 str 430 .replace(/\\/g, "\\\\") 431 .replace(/\'/g, "\\'") 432 .replace(/\n/g, "\\n") 433 .replace(/\r/g, "\\r") 434 .replace(/!/g, "\\041") 435 .replace(/[^\x20-\x7E]/g, escapeCharacter) + 436 "'" 437 ); 438 } 439 440 // Use single quote syntax. 441 return "'" + str + "'"; 442 }, 443 444 /** 445 * Escape util function for Windows systems. 446 * Credit: Google DevTools 447 */ 448 escapeStringWin(str) { 449 /* 450 Because cmd.exe parser and MS Crt arguments parsers use some of the 451 same escape characters, they can interact with each other in 452 horrible ways, the order of operations is critical. 453 454 Also see https://ss64.com/nt/syntax-esc.html for details on 455 escaping characters on Windows. 456 */ 457 const encapsChars = '^"'; 458 return ( 459 encapsChars + 460 str 461 // Replace all " with \" to ensure the first parser does not remove it. 462 .replace(/"/g, '\\"') 463 464 // Then escape all characters we are not sure about with ^ to ensure it 465 // gets to MS Crt parser safely. 466 // Note: Also do not escape unicode control (C) non-printable characters 467 // https://www.compart.com/en/unicode/category (this is captured with `\p{C}` and the `u` unicode flag) 468 .replace(/[^-a-zA-Z0-9\s_:=+~\/.',?;()*`\p{C}]/gu, "^$&") 469 470 // The % character is special because MS Crt parser will try and look for 471 // ENV variables and fill them in its place. We cannot escape them with % 472 // and cannot escape them with ^ (because it's cmd.exe's escape not MS Crt 473 // parser); So we can get cmd.exe parser to escape the character after it, 474 // if it is followed by a valid beginning character of an ENV variable. 475 // This ensures we do not try and double escape another ^ if it was placed 476 // by the previous replace. 477 .replace(/%(?=[a-zA-Z0-9_])/g, "%^") 478 479 // All other whitespace characters are replaced with a single space, as there 480 // is no way to enter their literal values in a command line, and they do break 481 // the command allowing for injection. 482 // Since want to keep line breaks, we need to exclude them in the regex (`[^\r\n]`), 483 // and use double negations to get the other whitespace chars (`[^\S]` translates 484 // to "not not whitespace") 485 .replace(/[^\S\r\n]/g, " ") 486 487 // Lastly we replace new lines with ^ and TWO new lines because the first 488 // new line is there to enact the escape command the second is the character 489 // to escape (in this case new line). 490 .replace(/\r?\n|\r/g, "^\n\n") + 491 encapsChars 492 ); 493 }, 494 }; 495 496 exports.CurlUtils = CurlUtils;