test_curl.js (10898B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 /** 7 * Tests utility functions contained in `source-utils.js` 8 */ 9 10 const curl = require("resource://devtools/client/shared/curl.js"); 11 const Curl = curl.Curl; 12 const CurlUtils = curl.CurlUtils; 13 14 // Test `Curl.generateCommand` headers forwarding/filtering 15 add_task(async function () { 16 const request = { 17 url: "https://example.com/form/", 18 method: "GET", 19 headers: [ 20 { name: "Host", value: "example.com" }, 21 { 22 name: "User-Agent", 23 value: 24 "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0", 25 }, 26 { name: "Accept", value: "*/*" }, 27 { name: "Accept-Language", value: "en-US,en;q=0.9" }, 28 { name: "Accept-Encoding", value: "gzip, deflate, br" }, 29 { name: "Origin", value: "https://example.com" }, 30 { name: "Connection", value: "keep-alive" }, 31 { name: "Referer", value: "https://example.com/home/" }, 32 { name: "Content-Type", value: "text/plain" }, 33 ], 34 responseHeaders: [], 35 httpVersion: "HTTP/2.0", 36 }; 37 38 const cmd = Curl.generateCommand(request); 39 const curlParams = parseCurl(cmd); 40 41 ok( 42 !headerTypeInParams(curlParams, "Host"), 43 "host header ignored - to be generated from url" 44 ); 45 ok( 46 exactHeaderInParams(curlParams, "Accept: */*"), 47 "accept header present in curl command" 48 ); 49 ok( 50 exactHeaderInParams( 51 curlParams, 52 "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0" 53 ), 54 "user-agent header present in curl command" 55 ); 56 ok( 57 exactHeaderInParams(curlParams, "Accept-Language: en-US,en;q=0.9"), 58 "accept-language header present in curl output" 59 ); 60 ok( 61 exactHeaderInParams(curlParams, "Accept-Encoding: gzip, deflate, br"), 62 "accept-encoding header present in curl output" 63 ); 64 ok( 65 exactHeaderInParams(curlParams, "Origin: https://example.com"), 66 "origin header present in curl output" 67 ); 68 ok( 69 exactHeaderInParams(curlParams, "Connection: keep-alive"), 70 "connection header present in curl output" 71 ); 72 ok( 73 exactHeaderInParams(curlParams, "Referer: https://example.com/home/"), 74 "referer header present in curl output" 75 ); 76 ok( 77 exactHeaderInParams(curlParams, "Content-Type: text/plain"), 78 "content-type header present in curl output" 79 ); 80 ok(!inParams(curlParams, "--data"), "no data param in GET curl output"); 81 ok( 82 !inParams(curlParams, "--data-raw"), 83 "no raw data param in GET curl output" 84 ); 85 }); 86 87 // Test `Curl.generateCommand` URL glob handling 88 add_task(async function () { 89 let request = { 90 url: "https://example.com/", 91 method: "GET", 92 headers: [], 93 responseHeaders: [], 94 httpVersion: "HTTP/2.0", 95 }; 96 97 let cmd = Curl.generateCommand(request); 98 let curlParams = parseCurl(cmd); 99 100 ok( 101 !inParams(curlParams, "--globoff"), 102 "no globoff param in curl output when not needed" 103 ); 104 105 request = { 106 url: "https://example.com/[]", 107 method: "GET", 108 headers: [], 109 responseHeaders: [], 110 httpVersion: "HTTP/2.0", 111 }; 112 113 cmd = Curl.generateCommand(request); 114 curlParams = parseCurl(cmd); 115 116 ok( 117 inParams(curlParams, "--globoff"), 118 "globoff param present in curl output when needed" 119 ); 120 }); 121 122 // Test `Curl.generateCommand` data POSTing 123 add_task(async function () { 124 const request = { 125 url: "https://example.com/form/", 126 method: "POST", 127 headers: [ 128 { name: "Content-Length", value: "1000" }, 129 { name: "Content-Type", value: "text/plain" }, 130 ], 131 responseHeaders: [], 132 httpVersion: "HTTP/2.0", 133 postDataText: "A piece of plain payload text", 134 }; 135 136 const cmd = Curl.generateCommand(request); 137 const curlParams = parseCurl(cmd); 138 139 ok( 140 !headerTypeInParams(curlParams, "Content-Length"), 141 "content-length header ignored - curl generates new one" 142 ); 143 ok( 144 exactHeaderInParams(curlParams, "Content-Type: text/plain"), 145 "content-type header present in curl output" 146 ); 147 ok( 148 inParams(curlParams, "--data-raw"), 149 '"--data-raw" param present in curl output' 150 ); 151 ok( 152 inParams(curlParams, `--data-raw ${quote(request.postDataText)}`), 153 "proper payload data present in output" 154 ); 155 }); 156 157 // Test `Curl.generateCommand` data POSTing - not post data 158 add_task(async function () { 159 const request = { 160 url: "https://example.com/form/", 161 method: "POST", 162 headers: [ 163 { name: "Content-Length", value: "1000" }, 164 { name: "Content-Type", value: "text/plain" }, 165 ], 166 responseHeaders: [], 167 httpVersion: "HTTP/2.0", 168 }; 169 170 const cmd = Curl.generateCommand(request); 171 const curlParams = parseCurl(cmd); 172 173 ok( 174 !inParams(curlParams, "--data-raw"), 175 '"--data-raw" param not present in curl output' 176 ); 177 178 const methodIndex = curlParams.indexOf("-X"); 179 180 ok( 181 methodIndex !== -1 && curlParams[methodIndex + 1] === "POST", 182 "request method explicit is POST" 183 ); 184 }); 185 186 // Test `Curl.generateCommand` multipart data POSTing 187 add_task(async function () { 188 const boundary = "----------14808"; 189 const request = { 190 url: "https://example.com/form/", 191 method: "POST", 192 headers: [ 193 { 194 name: "Content-Type", 195 value: `multipart/form-data; boundary=${boundary}`, 196 }, 197 ], 198 responseHeaders: [], 199 httpVersion: "HTTP/2.0", 200 postDataText: [ 201 `--${boundary}`, 202 'Content-Disposition: form-data; name="field_one"', 203 "", 204 "value_one", 205 `--${boundary}`, 206 'Content-Disposition: form-data; name="field_two"', 207 "", 208 "value two", 209 `--${boundary}--`, 210 "", 211 ].join("\r\n"), 212 }; 213 214 const cmd = Curl.generateCommand(request); 215 216 // Check content type 217 const contentTypePos = cmd.indexOf(headerParamPrefix("Content-Type")); 218 const contentTypeParam = headerParam( 219 `Content-Type: multipart/form-data; boundary=${boundary}` 220 ); 221 Assert.notStrictEqual( 222 contentTypePos, 223 -1, 224 "content type header present in curl output" 225 ); 226 equal( 227 cmd.substr(contentTypePos, contentTypeParam.length), 228 contentTypeParam, 229 "proper content type header present in curl output" 230 ); 231 232 // Check binary data 233 const dataBinaryPos = cmd.indexOf("--data-binary"); 234 const dataBinaryParam = `--data-binary ${isWin() ? "^\n " : "\\\n $"}${escapeNewline( 235 quote(request.postDataText) 236 )}`; 237 238 Assert.notStrictEqual( 239 dataBinaryPos, 240 -1, 241 "--data-binary param present in curl output" 242 ); 243 equal( 244 cmd.substr(dataBinaryPos, dataBinaryParam.length), 245 dataBinaryParam, 246 "proper multipart data present in curl output" 247 ); 248 }); 249 250 // Test `CurlUtils.removeBinaryDataFromMultipartText` doesn't change text data 251 add_task(async function () { 252 const boundary = "----------14808"; 253 const postTextLines = [ 254 `--${boundary}`, 255 'Content-Disposition: form-data; name="field_one"', 256 "", 257 "value_one", 258 `--${boundary}`, 259 'Content-Disposition: form-data; name="field_two"', 260 "", 261 "value two", 262 `--${boundary}--`, 263 "", 264 ]; 265 266 const cleanedText = CurlUtils.removeBinaryDataFromMultipartText( 267 postTextLines.join("\r\n"), 268 boundary 269 ); 270 equal( 271 cleanedText, 272 postTextLines.join("\r\n"), 273 "proper non-binary multipart text unchanged" 274 ); 275 }); 276 277 // Test `CurlUtils.removeBinaryDataFromMultipartText` removes binary data 278 add_task(async function () { 279 const boundary = "----------14808"; 280 const postTextLines = [ 281 `--${boundary}`, 282 'Content-Disposition: form-data; name="field_one"', 283 "", 284 "value_one", 285 `--${boundary}`, 286 'Content-Disposition: form-data; name="field_two"; filename="file_field_two.txt"', 287 "", 288 "file content", 289 `--${boundary}--`, 290 "", 291 ]; 292 293 const cleanedText = CurlUtils.removeBinaryDataFromMultipartText( 294 postTextLines.join("\r\n"), 295 boundary 296 ); 297 postTextLines.splice(7, 1); 298 equal( 299 cleanedText, 300 postTextLines.join("\r\n"), 301 "file content removed from multipart text" 302 ); 303 }); 304 305 // Test `Curl.generateCommand` add --compressed flag 306 add_task(async function () { 307 let request = { 308 url: "https://example.com/", 309 method: "GET", 310 headers: [], 311 responseHeaders: [], 312 httpVersion: "HTTP/2.0", 313 }; 314 315 let cmd = Curl.generateCommand(request); 316 let curlParams = parseCurl(cmd); 317 318 ok( 319 !inParams(curlParams, "--compressed"), 320 "no compressed param in curl output when not needed" 321 ); 322 323 request = { 324 url: "https://example.com/", 325 method: "GET", 326 headers: [], 327 responseHeaders: [{ name: "Content-Encoding", value: "gzip" }], 328 httpVersion: "HTTP/2.0", 329 }; 330 331 cmd = Curl.generateCommand(request); 332 curlParams = parseCurl(cmd); 333 334 ok( 335 inParams(curlParams, "--compressed"), 336 "compressed param present in curl output when needed" 337 ); 338 }); 339 340 function isWin() { 341 return Services.appinfo.OS === "WINNT"; 342 } 343 344 const QUOTE = isWin() ? '^"' : "'"; 345 346 // Quote a string, escape the quotes inside the string 347 function quote(str) { 348 let escaped; 349 if (isWin()) { 350 escaped = str.replace(new RegExp('"', "g"), `^\\${QUOTE}`); 351 } else { 352 escaped = str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`); 353 } 354 return QUOTE + escaped + QUOTE; 355 } 356 357 function escapeNewline(txt) { 358 if (isWin()) { 359 // For windows we replace new lines with ^ and TWO new lines because the first 360 // new line is there to enact the escape command the second is the character 361 // to escape (in this case new line). 362 return txt.replace(/\r?\n|\r/g, "^\n\n"); 363 } 364 return txt.replace(/\r/g, "\\r").replace(/\n/g, "\\n"); 365 } 366 367 // Header param is formatted as -H "Header: value" or -H 'Header: value' 368 function headerParam(h) { 369 return "-H " + quote(h); 370 } 371 372 // Header param prefix is formatted as `-H "HeaderName` or `-H 'HeaderName` 373 function headerParamPrefix(headerName) { 374 return `-H ${QUOTE}${headerName}`; 375 } 376 377 // If any params startswith `-H "HeaderName` or `-H 'HeaderName` 378 function headerTypeInParams(curlParams, headerName) { 379 return curlParams.some(param => 380 param.toLowerCase().startsWith(headerParamPrefix(headerName).toLowerCase()) 381 ); 382 } 383 384 function exactHeaderInParams(curlParams, header) { 385 return curlParams.some(param => param === headerParam(header)); 386 } 387 388 function inParams(curlParams, param) { 389 return curlParams.some(p => p.startsWith(param)); 390 } 391 392 // Parse complete curl command to array of params. Can be applied to simple headers/data, 393 // but will not on WIN with sophisticated values of --data-binary with e.g. escaped quotes 394 function parseCurl(curlCmd) { 395 // This monster regexp parses the command line into an array of arguments, 396 // recognizing quoted args with matching quotes and escaped quotes inside: 397 // [ "curl.exe 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ] 398 // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ] 399 const matchRe = /[-\.A-Za-z1-9]+(?: ([\^\"']+)(?:\\\1|.)*?\1)?/g; 400 return curlCmd.match(matchRe); 401 }