/* 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; }