summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/test/xpcshell/test_curl.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/test/xpcshell/test_curl.js')
-rw-r--r--devtools/client/shared/test/xpcshell/test_curl.js397
1 files changed, 397 insertions, 0 deletions
diff --git a/devtools/client/shared/test/xpcshell/test_curl.js b/devtools/client/shared/test/xpcshell/test_curl.js
new file mode 100644
index 0000000000..a2a6c3412e
--- /dev/null
+++ b/devtools/client/shared/test/xpcshell/test_curl.js
@@ -0,0 +1,397 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests utility functions contained in `source-utils.js`
+ */
+
+const curl = require("resource://devtools/client/shared/curl.js");
+const Curl = curl.Curl;
+const CurlUtils = curl.CurlUtils;
+
+// Test `Curl.generateCommand` headers forwarding/filtering
+add_task(async function () {
+ const request = {
+ url: "https://example.com/form/",
+ method: "GET",
+ headers: [
+ { name: "Host", value: "example.com" },
+ {
+ name: "User-Agent",
+ value:
+ "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0",
+ },
+ { name: "Accept", value: "*/*" },
+ { name: "Accept-Language", value: "en-US,en;q=0.5" },
+ { name: "Accept-Encoding", value: "gzip, deflate, br" },
+ { name: "Origin", value: "https://example.com" },
+ { name: "Connection", value: "keep-alive" },
+ { name: "Referer", value: "https://example.com/home/" },
+ { name: "Content-Type", value: "text/plain" },
+ ],
+ responseHeaders: [],
+ httpVersion: "HTTP/2.0",
+ };
+
+ const cmd = Curl.generateCommand(request);
+ const curlParams = parseCurl(cmd);
+
+ ok(
+ !headerTypeInParams(curlParams, "Host"),
+ "host header ignored - to be generated from url"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Accept: */*"),
+ "accept header present in curl command"
+ );
+ ok(
+ exactHeaderInParams(
+ curlParams,
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0"
+ ),
+ "user-agent header present in curl command"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Accept-Language: en-US,en;q=0.5"),
+ "accept-language header present in curl output"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Accept-Encoding: gzip, deflate, br"),
+ "accept-encoding header present in curl output"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Origin: https://example.com"),
+ "origin header present in curl output"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Connection: keep-alive"),
+ "connection header present in curl output"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Referer: https://example.com/home/"),
+ "referer header present in curl output"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Content-Type: text/plain"),
+ "content-type header present in curl output"
+ );
+ ok(!inParams(curlParams, "--data"), "no data param in GET curl output");
+ ok(
+ !inParams(curlParams, "--data-raw"),
+ "no raw data param in GET curl output"
+ );
+});
+
+// Test `Curl.generateCommand` URL glob handling
+add_task(async function () {
+ let request = {
+ url: "https://example.com/",
+ method: "GET",
+ headers: [],
+ responseHeaders: [],
+ httpVersion: "HTTP/2.0",
+ };
+
+ let cmd = Curl.generateCommand(request);
+ let curlParams = parseCurl(cmd);
+
+ ok(
+ !inParams(curlParams, "--globoff"),
+ "no globoff param in curl output when not needed"
+ );
+
+ request = {
+ url: "https://example.com/[]",
+ method: "GET",
+ headers: [],
+ responseHeaders: [],
+ httpVersion: "HTTP/2.0",
+ };
+
+ cmd = Curl.generateCommand(request);
+ curlParams = parseCurl(cmd);
+
+ ok(
+ inParams(curlParams, "--globoff"),
+ "globoff param present in curl output when needed"
+ );
+});
+
+// Test `Curl.generateCommand` data POSTing
+add_task(async function () {
+ const request = {
+ url: "https://example.com/form/",
+ method: "POST",
+ headers: [
+ { name: "Content-Length", value: "1000" },
+ { name: "Content-Type", value: "text/plain" },
+ ],
+ responseHeaders: [],
+ httpVersion: "HTTP/2.0",
+ postDataText: "A piece of plain payload text",
+ };
+
+ const cmd = Curl.generateCommand(request);
+ const curlParams = parseCurl(cmd);
+
+ ok(
+ !headerTypeInParams(curlParams, "Content-Length"),
+ "content-length header ignored - curl generates new one"
+ );
+ ok(
+ exactHeaderInParams(curlParams, "Content-Type: text/plain"),
+ "content-type header present in curl output"
+ );
+ ok(
+ inParams(curlParams, "--data-raw"),
+ '"--data-raw" param present in curl output'
+ );
+ ok(
+ inParams(curlParams, `--data-raw ${quote(request.postDataText)}`),
+ "proper payload data present in output"
+ );
+});
+
+// Test `Curl.generateCommand` data POSTing - not post data
+add_task(async function () {
+ const request = {
+ url: "https://example.com/form/",
+ method: "POST",
+ headers: [
+ { name: "Content-Length", value: "1000" },
+ { name: "Content-Type", value: "text/plain" },
+ ],
+ responseHeaders: [],
+ httpVersion: "HTTP/2.0",
+ };
+
+ const cmd = Curl.generateCommand(request);
+ const curlParams = parseCurl(cmd);
+
+ ok(
+ !inParams(curlParams, "--data-raw"),
+ '"--data-raw" param not present in curl output'
+ );
+
+ const methodIndex = curlParams.indexOf("-X");
+
+ ok(
+ methodIndex !== -1 && curlParams[methodIndex + 1] === "POST",
+ "request method explicit is POST"
+ );
+});
+
+// Test `Curl.generateCommand` multipart data POSTing
+add_task(async function () {
+ const boundary = "----------14808";
+ const request = {
+ url: "https://example.com/form/",
+ method: "POST",
+ headers: [
+ {
+ name: "Content-Type",
+ value: `multipart/form-data; boundary=${boundary}`,
+ },
+ ],
+ responseHeaders: [],
+ httpVersion: "HTTP/2.0",
+ postDataText: [
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_one"',
+ "",
+ "value_one",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_two"',
+ "",
+ "value two",
+ `--${boundary}--`,
+ "",
+ ].join("\r\n"),
+ };
+
+ const cmd = Curl.generateCommand(request);
+
+ // Check content type
+ const contentTypePos = cmd.indexOf(headerParamPrefix("Content-Type"));
+ const contentTypeParam = headerParam(
+ `Content-Type: multipart/form-data; boundary=${boundary}`
+ );
+ Assert.notStrictEqual(
+ contentTypePos,
+ -1,
+ "content type header present in curl output"
+ );
+ equal(
+ cmd.substr(contentTypePos, contentTypeParam.length),
+ contentTypeParam,
+ "proper content type header present in curl output"
+ );
+
+ // Check binary data
+ const dataBinaryPos = cmd.indexOf("--data-binary");
+ const dataBinaryParam = `--data-binary ${isWin() ? "" : "$"}${escapeNewline(
+ quote(request.postDataText)
+ )}`;
+ Assert.notStrictEqual(
+ dataBinaryPos,
+ -1,
+ "--data-binary param present in curl output"
+ );
+ equal(
+ cmd.substr(dataBinaryPos, dataBinaryParam.length),
+ dataBinaryParam,
+ "proper multipart data present in curl output"
+ );
+});
+
+// Test `CurlUtils.removeBinaryDataFromMultipartText` doesn't change text data
+add_task(async function () {
+ const boundary = "----------14808";
+ const postTextLines = [
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_one"',
+ "",
+ "value_one",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_two"',
+ "",
+ "value two",
+ `--${boundary}--`,
+ "",
+ ];
+
+ const cleanedText = CurlUtils.removeBinaryDataFromMultipartText(
+ postTextLines.join("\r\n"),
+ boundary
+ );
+ equal(
+ cleanedText,
+ postTextLines.join("\r\n"),
+ "proper non-binary multipart text unchanged"
+ );
+});
+
+// Test `CurlUtils.removeBinaryDataFromMultipartText` removes binary data
+add_task(async function () {
+ const boundary = "----------14808";
+ const postTextLines = [
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_one"',
+ "",
+ "value_one",
+ `--${boundary}`,
+ 'Content-Disposition: form-data; name="field_two"; filename="file_field_two.txt"',
+ "",
+ "file content",
+ `--${boundary}--`,
+ "",
+ ];
+
+ const cleanedText = CurlUtils.removeBinaryDataFromMultipartText(
+ postTextLines.join("\r\n"),
+ boundary
+ );
+ postTextLines.splice(7, 1);
+ equal(
+ cleanedText,
+ postTextLines.join("\r\n"),
+ "file content removed from multipart text"
+ );
+});
+
+// Test `Curl.generateCommand` add --compressed flag
+add_task(async function () {
+ let request = {
+ url: "https://example.com/",
+ method: "GET",
+ headers: [],
+ responseHeaders: [],
+ httpVersion: "HTTP/2.0",
+ };
+
+ let cmd = Curl.generateCommand(request);
+ let curlParams = parseCurl(cmd);
+
+ ok(
+ !inParams(curlParams, "--compressed"),
+ "no compressed param in curl output when not needed"
+ );
+
+ request = {
+ url: "https://example.com/",
+ method: "GET",
+ headers: [],
+ responseHeaders: [{ name: "Content-Encoding", value: "gzip" }],
+ httpVersion: "HTTP/2.0",
+ };
+
+ cmd = Curl.generateCommand(request);
+ curlParams = parseCurl(cmd);
+
+ ok(
+ inParams(curlParams, "--compressed"),
+ "compressed param present in curl output when needed"
+ );
+});
+
+function isWin() {
+ return Services.appinfo.OS === "WINNT";
+}
+
+const QUOTE = isWin() ? '"' : "'";
+
+// Quote a string, escape the quotes inside the string
+function quote(str) {
+ let escaped;
+ if (isWin()) {
+ escaped = str.replace(new RegExp(QUOTE, "g"), `${QUOTE}${QUOTE}`);
+ } else {
+ escaped = str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`);
+ }
+ return QUOTE + escaped + QUOTE;
+}
+
+function escapeNewline(txt) {
+ if (isWin()) {
+ // Add `"` to close quote, then escape newline outside of quote, then start new quote
+ return txt.replace(/[\r\n]{1,2}/g, '"^$&$&"');
+ }
+ return txt.replace(/\r/g, "\\r").replace(/\n/g, "\\n");
+}
+
+// Header param is formatted as -H "Header: value" or -H 'Header: value'
+function headerParam(h) {
+ return "-H " + quote(h);
+}
+
+// Header param prefix is formatted as `-H "HeaderName` or `-H 'HeaderName`
+function headerParamPrefix(headerName) {
+ return `-H ${QUOTE}${headerName}`;
+}
+
+// If any params startswith `-H "HeaderName` or `-H 'HeaderName`
+function headerTypeInParams(curlParams, headerName) {
+ return curlParams.some(param =>
+ param.toLowerCase().startsWith(headerParamPrefix(headerName).toLowerCase())
+ );
+}
+
+function exactHeaderInParams(curlParams, header) {
+ return curlParams.some(param => param === headerParam(header));
+}
+
+function inParams(curlParams, param) {
+ return curlParams.some(p => p.startsWith(param));
+}
+
+// Parse complete curl command to array of params. Can be applied to simple headers/data,
+// but will not on WIN with sophisticated values of --data-binary with e.g. escaped quotes
+function parseCurl(curlCmd) {
+ // This monster regexp parses the command line into an array of arguments,
+ // recognizing quoted args with matching quotes and escaped quotes inside:
+ // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ]
+ const matchRe = /[-A-Za-z1-9]+(?: \$?([\"'])(?:\\\1|.)*?\1)?/g;
+ return curlCmd.match(matchRe);
+}