diff options
Diffstat (limited to 'devtools/client/netmonitor/test/browser_net_curl-utils.js')
-rw-r--r-- | devtools/client/netmonitor/test/browser_net_curl-utils.js | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/test/browser_net_curl-utils.js b/devtools/client/netmonitor/test/browser_net_curl-utils.js new file mode 100644 index 0000000000..32b7aca316 --- /dev/null +++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js @@ -0,0 +1,385 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests Curl Utils functionality. + */ + +const { + Curl, + CurlUtils, +} = require("resource://devtools/client/shared/curl.js"); + +add_task(async function () { + const { tab, monitor } = await initNetMonitor(HTTPS_CURL_UTILS_URL, { + requestCount: 1, + }); + info("Starting test... "); + + const { store, windowRequire, connector } = monitor.panelWin; + const Actions = windowRequire("devtools/client/netmonitor/src/actions/index"); + const { getSortedRequests } = windowRequire( + "devtools/client/netmonitor/src/selectors/index" + ); + const { getLongString, requestData } = connector; + + store.dispatch(Actions.batchEnable(false)); + + const wait = waitForNetworkEvents(monitor, 6); + await SpecialPowers.spawn( + tab.linkedBrowser, + [HTTPS_SIMPLE_SJS], + async function (url) { + content.wrappedJSObject.performRequests(url); + } + ); + await wait; + + const requests = { + get: getSortedRequests(store.getState())[0], + post: getSortedRequests(store.getState())[1], + postJson: getSortedRequests(store.getState())[2], + patch: getSortedRequests(store.getState())[3], + multipart: getSortedRequests(store.getState())[4], + multipartForm: getSortedRequests(store.getState())[5], + }; + + let data = await createCurlData(requests.get, getLongString, requestData); + testFindHeader(data); + + data = await createCurlData(requests.post, getLongString, requestData); + testIsUrlEncodedRequest(data); + testWritePostDataTextParams(data); + testWriteEmptyPostDataTextParams(data); + testDataArgumentOnGeneratedCommand(data); + + data = await createCurlData(requests.patch, getLongString, requestData); + testWritePostDataTextParams(data); + testDataArgumentOnGeneratedCommand(data); + + data = await createCurlData(requests.postJson, getLongString, requestData); + testDataEscapeOnGeneratedCommand(data); + + data = await createCurlData(requests.multipart, getLongString, requestData); + testIsMultipartRequest(data); + testGetMultipartBoundary(data); + testMultiPartHeaders(data); + testRemoveBinaryDataFromMultipartText(data); + + data = await createCurlData( + requests.multipartForm, + getLongString, + requestData + ); + testMultiPartHeaders(data); + + testGetHeadersFromMultipartText({ + postDataText: "Content-Type: text/plain\r\n\r\n", + }); + + if (Services.appinfo.OS != "WINNT") { + testEscapeStringPosix(); + } else { + testEscapeStringWin(); + } + + await teardown(monitor); +}); + +function testIsUrlEncodedRequest(data) { + const isUrlEncoded = CurlUtils.isUrlEncodedRequest(data); + ok(isUrlEncoded, "Should return true for url encoded requests."); +} + +function testIsMultipartRequest(data) { + const isMultipart = CurlUtils.isMultipartRequest(data); + ok(isMultipart, "Should return true for multipart/form-data requests."); +} + +function testFindHeader(data) { + const { headers } = data; + const hostName = CurlUtils.findHeader(headers, "Host"); + const requestedWithLowerCased = CurlUtils.findHeader( + headers, + "x-requested-with" + ); + const doesNotExist = CurlUtils.findHeader(headers, "X-Does-Not-Exist"); + + is( + hostName, + "example.com", + "Header with name 'Host' should be found in the request array." + ); + is( + requestedWithLowerCased, + "XMLHttpRequest", + "The search should be case insensitive." + ); + is(doesNotExist, null, "Should return null when a header is not found."); +} + +function testMultiPartHeaders(data) { + const { headers } = data; + const contentType = CurlUtils.findHeader(headers, "Content-Type"); + + ok( + contentType.startsWith("multipart/form-data; boundary="), + "Multi-part content type header is present in headers array" + ); +} + +function testWritePostDataTextParams(data) { + const params = CurlUtils.writePostDataTextParams(data.postDataText); + is( + params, + "param1=value1¶m2=value2¶m3=value3", + "Should return a serialized representation of the request parameters" + ); +} + +function testWriteEmptyPostDataTextParams(data) { + const params = CurlUtils.writePostDataTextParams(null); + is(params, "", "Should return a empty string when no parameters provided"); +} + +function testDataArgumentOnGeneratedCommand(data) { + const curlCommand = Curl.generateCommand(data); + ok( + curlCommand.includes("--data-raw"), + "Should return a curl command with --data-raw" + ); +} + +function testDataEscapeOnGeneratedCommand(data) { + const paramsWin = `--data-raw "{""param1"":""value1"",""param2"":""value2""}"`; + const paramsPosix = `--data-raw '{"param1":"value1","param2":"value2"}'`; + + let curlCommand = Curl.generateCommand(data, "WINNT"); + ok( + curlCommand.includes(paramsWin), + "Should return a curl command with --data-raw escaped for Windows systems" + ); + + curlCommand = Curl.generateCommand(data, "Linux"); + ok( + curlCommand.includes(paramsPosix), + "Should return a curl command with --data-raw escaped for Posix systems" + ); +} + +function testGetMultipartBoundary(data) { + const boundary = CurlUtils.getMultipartBoundary(data); + ok( + /-{3,}\w+/.test(boundary), + "A boundary string should be found in a multipart request." + ); +} + +function testRemoveBinaryDataFromMultipartText(data) { + const generatedBoundary = CurlUtils.getMultipartBoundary(data); + const text = data.postDataText; + const binaryRemoved = CurlUtils.removeBinaryDataFromMultipartText( + text, + generatedBoundary + ); + const boundary = "--" + generatedBoundary; + + const EXPECTED_POSIX_RESULT = [ + "$'", + boundary, + "\\r\\n", + 'Content-Disposition: form-data; name="param1"', + "\\r\\n\\r\\n", + "value1", + "\\r\\n", + boundary, + "\\r\\n", + 'Content-Disposition: form-data; name="file"; filename="filename.png"', + "\\r\\n", + "Content-Type: image/png", + "\\r\\n\\r\\n", + boundary + "--", + "\\r\\n", + "'", + ].join(""); + + const EXPECTED_WIN_RESULT = [ + '"', + boundary, + '"^\u000d\u000A\u000d\u000A"', + 'Content-Disposition: form-data; name=""param1""', + '"^\u000d\u000A\u000d\u000A""^\u000d\u000A\u000d\u000A"', + "value1", + '"^\u000d\u000A\u000d\u000A"', + boundary, + '"^\u000d\u000A\u000d\u000A"', + 'Content-Disposition: form-data; name=""file""; filename=""filename.png""', + '"^\u000d\u000A\u000d\u000A"', + "Content-Type: image/png", + '"^\u000d\u000A\u000d\u000A""^\u000d\u000A\u000d\u000A"', + boundary + "--", + '"^\u000d\u000A\u000d\u000A"', + '"', + ].join(""); + + if (Services.appinfo.OS != "WINNT") { + is( + CurlUtils.escapeStringPosix(binaryRemoved), + EXPECTED_POSIX_RESULT, + "The mulitpart request payload should not contain binary data." + ); + } else { + is( + CurlUtils.escapeStringWin(binaryRemoved), + EXPECTED_WIN_RESULT, + "WinNT: The mulitpart request payload should not contain binary data." + ); + } +} + +function testGetHeadersFromMultipartText(data) { + const headers = CurlUtils.getHeadersFromMultipartText(data.postDataText); + + ok(Array.isArray(headers), "Should return an array."); + ok(!!headers.length, "There should exist at least one request header."); + is( + headers[0].name, + "Content-Type", + "The first header name should be 'Content-Type'." + ); +} + +function testEscapeStringPosix() { + const surroundedWithQuotes = "A simple string"; + is( + CurlUtils.escapeStringPosix(surroundedWithQuotes), + "'A simple string'", + "The string should be surrounded with single quotes." + ); + + const singleQuotes = "It's unusual to put crickets in your coffee."; + is( + CurlUtils.escapeStringPosix(singleQuotes), + "$'It\\'s unusual to put crickets in your coffee.'", + "Single quotes should be escaped." + ); + + const escapeChar = "'!ls:q:gs|ls|;ping 8.8.8.8;|"; + is( + CurlUtils.escapeStringPosix(escapeChar), + "$'\\'\\041ls:q:gs|ls|;ping 8.8.8.8;|'", + "'!' should be escaped." + ); + + const newLines = "Line 1\r\nLine 2\u000d\u000ALine3"; + is( + CurlUtils.escapeStringPosix(newLines), + "$'Line 1\\r\\nLine 2\\r\\nLine3'", + "Newlines should be escaped." + ); + + const controlChars = "\u0007 \u0009 \u000C \u001B"; + is( + CurlUtils.escapeStringPosix(controlChars), + "$'\\x07 \\x09 \\x0c \\x1b'", + "Control characters should be escaped." + ); + + // æ ø ü ß ö é + const extendedAsciiChars = + "\xc3\xa6 \xc3\xb8 \xc3\xbc \xc3\x9f \xc3\xb6 \xc3\xa9"; + is( + CurlUtils.escapeStringPosix(extendedAsciiChars), + "$'\\xc3\\xa6 \\xc3\\xb8 \\xc3\\xbc \\xc3\\x9f \\xc3\\xb6 \\xc3\\xa9'", + "Character codes outside of the decimal range 32 - 126 should be escaped." + ); +} + +function testEscapeStringWin() { + const surroundedWithDoubleQuotes = "A simple string"; + is( + CurlUtils.escapeStringWin(surroundedWithDoubleQuotes), + '"A simple string"', + "The string should be surrounded with double quotes." + ); + + const doubleQuotes = 'Quote: "Time is an illusion. Lunchtime doubly so."'; + is( + CurlUtils.escapeStringWin(doubleQuotes), + '"Quote: ""Time is an illusion. Lunchtime doubly so."""', + "Double quotes should be escaped." + ); + + const percentSigns = "%TEMP% %@foo% %2XX% %_XX% %?XX%"; + is( + CurlUtils.escapeStringWin(percentSigns), + '"^%^TEMP^% ^%^@foo^% ^%^2XX^% ^%^_XX^% ^%?XX^%"', + "Percent signs should be escaped." + ); + + const backslashes = "\\A simple string\\"; + is( + CurlUtils.escapeStringWin(backslashes), + '"\\\\A simple string\\\\"', + "Backslashes should be escaped." + ); + + const newLines = "line1\r\nline2\r\rline3\n\nline4"; + is( + CurlUtils.escapeStringWin(newLines), + '"line1"^\r\n\r\n"line2"^\r\n\r\n""^\r\n\r\n"line3"^\r\n\r\n""^\r\n\r\n"line4"', + "Newlines should be escaped." + ); + + const dollarSignCommand = "$(calc.exe)"; + is( + CurlUtils.escapeStringWin(dollarSignCommand), + '"\\$(calc.exe)"', + "Dollar sign should be escaped." + ); + + const tickSignCommand = "`$(calc.exe)"; + is( + CurlUtils.escapeStringWin(tickSignCommand), + '"\\`\\$(calc.exe)"', + "Both the tick and dollar signs should be escaped." + ); + + const evilCommand = `query=evil\r\rcmd" /c timeout /t 3 & calc.exe\r\r`; + is( + CurlUtils.escapeStringWin(evilCommand), + '"query=evil"^\r\n\r\n""^\r\n\r\n"cmd"" /c timeout /t 3 & calc.exe"^\r\n\r\n""^\r\n\r\n""', + "The evil command is escaped properly" + ); +} + +async function createCurlData(selected, getLongString, requestData) { + const { id, url, method, httpVersion } = selected; + + // Create a sanitized object for the Curl command generator. + const data = { + url, + method, + headers: [], + httpVersion, + postDataText: null, + }; + + const requestHeaders = await requestData(id, "requestHeaders"); + // Fetch header values. + for (const { name, value } of requestHeaders.headers) { + const text = await getLongString(value); + data.headers.push({ name, value: text }); + } + + const requestPostData = await requestData(id, "requestPostData"); + // Fetch the request payload. + if (requestPostData) { + const postData = requestPostData.postData.text; + data.postDataText = await getLongString(postData); + } + + return data; +} |