browser_net_curl-utils.js (11742B)
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 Curl Utils functionality. 8 */ 9 10 const { 11 Curl, 12 CurlUtils, 13 } = require("resource://devtools/client/shared/curl.js"); 14 15 add_task(async function () { 16 const { tab, monitor } = await initNetMonitor(HTTPS_CURL_UTILS_URL, { 17 requestCount: 1, 18 }); 19 info("Starting test... "); 20 21 const { store, windowRequire, connector } = monitor.panelWin; 22 const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); 23 const { getSortedRequests } = windowRequire( 24 "devtools/client/netmonitor/src/selectors/index" 25 ); 26 const { getLongString, requestData } = connector; 27 28 store.dispatch(Actions.batchEnable(false)); 29 30 const wait = waitForNetworkEvents(monitor, 6); 31 await SpecialPowers.spawn( 32 tab.linkedBrowser, 33 [HTTPS_SIMPLE_SJS], 34 async function (url) { 35 content.wrappedJSObject.performRequests(url); 36 } 37 ); 38 await wait; 39 40 const requests = { 41 get: getSortedRequests(store.getState())[0], 42 post: getSortedRequests(store.getState())[1], 43 postJson: getSortedRequests(store.getState())[2], 44 patch: getSortedRequests(store.getState())[3], 45 multipart: getSortedRequests(store.getState())[4], 46 multipartForm: getSortedRequests(store.getState())[5], 47 }; 48 49 let data = await createCurlData(requests.get, getLongString, requestData); 50 testFindHeader(data); 51 52 data = await createCurlData(requests.post, getLongString, requestData); 53 testIsUrlEncodedRequest(data); 54 testWritePostDataTextParams(data); 55 testWriteEmptyPostDataTextParams(data); 56 testDataArgumentOnGeneratedCommand(data); 57 58 data = await createCurlData(requests.patch, getLongString, requestData); 59 testWritePostDataTextParams(data); 60 testDataArgumentOnGeneratedCommand(data); 61 62 data = await createCurlData(requests.postJson, getLongString, requestData); 63 testDataEscapeOnGeneratedCommand(data); 64 65 data = await createCurlData(requests.multipart, getLongString, requestData); 66 testIsMultipartRequest(data); 67 testGetMultipartBoundary(data); 68 testMultiPartHeaders(data); 69 testRemoveBinaryDataFromMultipartText(data); 70 71 data = await createCurlData( 72 requests.multipartForm, 73 getLongString, 74 requestData 75 ); 76 testMultiPartHeaders(data); 77 78 testGetHeadersFromMultipartText({ 79 postDataText: "Content-Type: text/plain\r\n\r\n", 80 }); 81 82 if (Services.appinfo.OS != "WINNT") { 83 testEscapeStringPosix(); 84 } else { 85 testEscapeStringWin(); 86 } 87 88 await teardown(monitor); 89 }); 90 91 function testIsUrlEncodedRequest(data) { 92 const isUrlEncoded = CurlUtils.isUrlEncodedRequest(data); 93 ok(isUrlEncoded, "Should return true for url encoded requests."); 94 } 95 96 function testIsMultipartRequest(data) { 97 const isMultipart = CurlUtils.isMultipartRequest(data); 98 ok(isMultipart, "Should return true for multipart/form-data requests."); 99 } 100 101 function testFindHeader(data) { 102 const { headers } = data; 103 const hostName = CurlUtils.findHeader(headers, "Host"); 104 const requestedWithLowerCased = CurlUtils.findHeader( 105 headers, 106 "x-requested-with" 107 ); 108 const doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist"); 109 110 is( 111 hostName, 112 "example.com", 113 "Header with name 'Host' should be found in the request array." 114 ); 115 is( 116 requestedWithLowerCased, 117 "XMLHttpRequest", 118 "The search should be case insensitive." 119 ); 120 is(doesNotExist, null, "Should return null when a header is not found."); 121 } 122 123 function testMultiPartHeaders(data) { 124 const { headers } = data; 125 const contentType = CurlUtils.findHeader(headers, "Content-Type"); 126 127 ok( 128 contentType.startsWith("multipart/form-data; boundary="), 129 "Multi-part content type header is present in headers array" 130 ); 131 } 132 133 function testWritePostDataTextParams(data) { 134 const params = CurlUtils.writePostDataTextParams(data.postDataText); 135 is( 136 params, 137 "param1=value1¶m2=value2¶m3=value3", 138 "Should return a serialized representation of the request parameters" 139 ); 140 } 141 142 function testWriteEmptyPostDataTextParams() { 143 const params = CurlUtils.writePostDataTextParams(null); 144 is(params, "", "Should return a empty string when no parameters provided"); 145 } 146 147 function testDataArgumentOnGeneratedCommand(data) { 148 const curlCommand = Curl.generateCommand(data); 149 ok( 150 curlCommand.includes("--data-raw"), 151 "Should return a curl command with --data-raw" 152 ); 153 } 154 155 function testDataEscapeOnGeneratedCommand(data) { 156 const paramsWin = `--data-raw ^"^{^\\^"param1^\\^":^\\^"value1^\\^",^\\^"param2^\\^":^\\^"value2^\\^"^}^`; 157 const paramsPosix = `--data-raw '{"param1":"value1","param2":"value2"}'`; 158 159 let curlCommand = Curl.generateCommand(data, "WINNT"); 160 ok( 161 curlCommand.includes(paramsWin), 162 "Should return a curl command with --data-raw escaped for Windows systems" 163 ); 164 165 curlCommand = Curl.generateCommand(data, "Linux"); 166 ok( 167 curlCommand.includes(paramsPosix), 168 "Should return a curl command with --data-raw escaped for Posix systems" 169 ); 170 } 171 172 function testGetMultipartBoundary(data) { 173 const boundary = CurlUtils.getMultipartBoundary(data); 174 ok( 175 /-{3,}\w+/.test(boundary), 176 "A boundary string should be found in a multipart request." 177 ); 178 } 179 180 function testRemoveBinaryDataFromMultipartText(data) { 181 const generatedBoundary = CurlUtils.getMultipartBoundary(data); 182 const text = data.postDataText; 183 const binaryRemoved = CurlUtils.removeBinaryDataFromMultipartText( 184 text, 185 generatedBoundary 186 ); 187 const boundary = "--" + generatedBoundary; 188 189 const EXPECTED_POSIX_RESULT = [ 190 "$'", 191 boundary, 192 "\\r\\n", 193 'Content-Disposition: form-data; name="param1"', 194 "\\r\\n\\r\\n", 195 "value1", 196 "\\r\\n", 197 boundary, 198 "\\r\\n", 199 'Content-Disposition: form-data; name="file"; filename="filename.png"', 200 "\\r\\n", 201 "Content-Type: image/png", 202 "\\r\\n\\r\\n", 203 boundary + "--", 204 "\\r\\n", 205 "'", 206 ].join(""); 207 208 const EXPECTED_WIN_RESULT = [ 209 '^"', 210 boundary, 211 "^\u000A\u000A", 212 'Content-Disposition: form-data; name=^\\^"param1^\\^"', 213 "^\u000A\u000A^\u000A\u000A", 214 "value1", 215 "^\u000A\u000A", 216 boundary, 217 "^\u000A\u000A", 218 'Content-Disposition: form-data; name=^\\^"file^\\^"; filename=^\\^"filename.png^\\^"', 219 "^\u000A\u000A", 220 "Content-Type: image/png", 221 "^\u000A\u000A^\u000A\u000A", 222 boundary + "--", 223 "^\u000A\u000A", 224 '^"', 225 ].join(""); 226 227 if (Services.appinfo.OS != "WINNT") { 228 is( 229 CurlUtils.escapeStringPosix(binaryRemoved), 230 EXPECTED_POSIX_RESULT, 231 "The mulitpart request payload should not contain binary data." 232 ); 233 } else { 234 is( 235 CurlUtils.escapeStringWin(binaryRemoved), 236 EXPECTED_WIN_RESULT, 237 "WinNT: The mulitpart request payload should not contain binary data." 238 ); 239 } 240 } 241 242 function testGetHeadersFromMultipartText(data) { 243 const headers = CurlUtils.getHeadersFromMultipartText(data.postDataText); 244 245 ok(Array.isArray(headers), "Should return an array."); 246 ok(!!headers.length, "There should exist at least one request header."); 247 is( 248 headers[0].name, 249 "Content-Type", 250 "The first header name should be 'Content-Type'." 251 ); 252 } 253 254 function testEscapeStringPosix() { 255 const surroundedWithQuotes = "A simple string"; 256 is( 257 CurlUtils.escapeStringPosix(surroundedWithQuotes), 258 "'A simple string'", 259 "The string should be surrounded with single quotes." 260 ); 261 262 const singleQuotes = "It's unusual to put crickets in your coffee."; 263 is( 264 CurlUtils.escapeStringPosix(singleQuotes), 265 "$'It\\'s unusual to put crickets in your coffee.'", 266 "Single quotes should be escaped." 267 ); 268 269 const escapeChar = "'!ls:q:gs|ls|;ping 8.8.8.8;|"; 270 is( 271 CurlUtils.escapeStringPosix(escapeChar), 272 "$'\\'\\041ls:q:gs|ls|;ping 8.8.8.8;|'", 273 "'!' should be escaped." 274 ); 275 276 const escapeBangOnlyChar = "!"; 277 is( 278 CurlUtils.escapeStringPosix(escapeBangOnlyChar), 279 "$'\\041'", 280 "'!' should be escaped." 281 ); 282 283 const newLines = "Line 1\r\nLine 2\u000d\u000ALine3"; 284 is( 285 CurlUtils.escapeStringPosix(newLines), 286 "$'Line 1\\r\\nLine 2\\r\\nLine3'", 287 "Newlines should be escaped." 288 ); 289 290 const controlChars = "\u0007 \u0009 \u000C \u001B"; 291 is( 292 CurlUtils.escapeStringPosix(controlChars), 293 "$'\\x07 \\x09 \\x0c \\x1b'", 294 "Control characters should be escaped." 295 ); 296 297 // æ ø ü ß ö é 298 const extendedAsciiChars = 299 "\xc3\xa6 \xc3\xb8 \xc3\xbc \xc3\x9f \xc3\xb6 \xc3\xa9"; 300 is( 301 CurlUtils.escapeStringPosix(extendedAsciiChars), 302 "$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'", 303 "Character codes outside of the decimal range 32 - 126 should be escaped." 304 ); 305 } 306 307 function testEscapeStringWin() { 308 const surroundedWithDoubleQuotes = "A simple string"; 309 is( 310 CurlUtils.escapeStringWin(surroundedWithDoubleQuotes), 311 '^\"A simple string^\"', 312 "The string should be surrounded with double quotes." 313 ); 314 315 const doubleQuotes = 'Quote: "Time is an illusion. Lunchtime doubly so."'; 316 is( 317 CurlUtils.escapeStringWin(doubleQuotes), 318 '^\"Quote: ^\\^\"Time is an illusion. Lunchtime doubly so.^\\^\"^\"', 319 "Double quotes should be escaped." 320 ); 321 322 const percentSigns = "%TEMP% %@foo% %2XX% %_XX% %?XX%"; 323 is( 324 CurlUtils.escapeStringWin(percentSigns), 325 '^\"^%^TEMP^% ^%^@foo^% ^%^2XX^% ^%^_XX^% ^%?XX^%^\"', 326 "Percent signs should be escaped." 327 ); 328 329 const backslashes = "\\A simple string\\"; 330 is( 331 CurlUtils.escapeStringWin(backslashes), 332 '^\"^\\A simple string^\\^\"', 333 "Backslashes should be escaped." 334 ); 335 336 const newLines = "line1\r\nline2\r\rline3\n\nline4"; 337 is( 338 CurlUtils.escapeStringWin(newLines), 339 '^\"line1^\n\nline2^\n\n^\n\nline3^\n\n^\n\nline4^\"', 340 "Newlines should be escaped." 341 ); 342 343 const dollarSignCommand = "$(calc.exe)"; 344 is( 345 CurlUtils.escapeStringWin(dollarSignCommand), 346 '^\"^$(calc.exe)^\"', 347 "Dollar sign should be escaped." 348 ); 349 350 const tickSignCommand = "`$(calc.exe)"; 351 is( 352 CurlUtils.escapeStringWin(tickSignCommand), 353 '^\"`^$(calc.exe)^\"', 354 "Both the tick and dollar signs should be escaped." 355 ); 356 357 const evilCommand = `query=evil\r\rcmd" /c timeout /t 3 & calc.exe\r\r`; 358 is( 359 CurlUtils.escapeStringWin(evilCommand), 360 '^\"query=evil^\n\n^\n\ncmd^\\^\" /c timeout /t 3 ^& calc.exe^\n\n^\n\n^\"', 361 "The evil command is escaped properly" 362 ); 363 364 // Control characters https://www.ascii-code.com/characters/control-characters 365 const containsControlChars = " - \u0007 \u0010 \u0014 \u001B \x1a - "; 366 is( 367 CurlUtils.escapeStringWin(containsControlChars), 368 '^\" - \u0007 \u0010 \u0014 \u001b \u001a - ^\"', 369 "Control characters should not be escaped with ^." 370 ); 371 372 const controlCharsWithWhitespaces = " -\tcalc.exe\f- "; 373 is( 374 CurlUtils.escapeStringWin(controlCharsWithWhitespaces), 375 '^\" - calc.exe - ^\"', 376 "Control (non-printable) characters which are whitespace like charaters e.g (tab & form feed)" 377 ); 378 } 379 380 async function createCurlData(selected, getLongString, requestData) { 381 const { id, url, method, httpVersion } = selected; 382 383 // Create a sanitized object for the Curl command generator. 384 const data = { 385 url, 386 method, 387 headers: [], 388 httpVersion, 389 postDataText: null, 390 }; 391 392 const requestHeaders = await requestData(id, "requestHeaders"); 393 // Fetch header values. 394 for (const { name, value } of requestHeaders.headers) { 395 const text = await getLongString(value); 396 data.headers.push({ name, value: text }); 397 } 398 399 const requestPostData = await requestData(id, "requestPostData"); 400 // Fetch the request payload. 401 if (requestPostData) { 402 const postData = requestPostData.postData.text; 403 data.postDataText = await getLongString(postData); 404 } 405 406 return data; 407 }