browser_net_copy_as_curl.js (7348B)
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 if Copy as cURL works. 8 */ 9 10 const POST_PAYLOAD = "Plaintext value as a payload"; 11 12 add_task(async function () { 13 const { tab, monitor } = await initNetMonitor(HTTPS_CURL_URL, { 14 requestCount: 1, 15 }); 16 // disable sending idempotency header for POST request 17 await pushPref("network.http.idempotencyKey.enabled", false); 18 info("Starting test... "); 19 20 // Different quote chars are used for Windows and POSIX 21 const QUOTE_WIN = '^"'; 22 const QUOTE_POSIX = "'"; 23 24 const isWin = Services.appinfo.OS === "WINNT"; 25 const testData = isWin 26 ? [ 27 { 28 menuItemId: "request-list-context-copy-as-curl-win", 29 data: buildTestData(QUOTE_WIN, true), 30 }, 31 { 32 menuItemId: "request-list-context-copy-as-curl-posix", 33 data: buildTestData(QUOTE_POSIX, false), 34 }, 35 ] 36 : [ 37 { 38 menuItemId: "request-list-context-copy-as-curl", 39 data: buildTestData(QUOTE_POSIX, false), 40 }, 41 ]; 42 43 await testForPlatform(tab, monitor, testData); 44 45 await teardown(monitor); 46 }); 47 48 function buildTestData(QUOTE, isWin) { 49 // Quote a string, escape the quotes inside the string 50 function quote(str) { 51 return QUOTE + str.replace(new RegExp(QUOTE, "g"), `\\${QUOTE}`) + QUOTE; 52 } 53 54 // Header param is formatted as -H "Header: value" or -H 'Header: value' 55 function header(h) { 56 return "-H " + quote(h); 57 } 58 59 const CMD = isWin ? "curl.exe " : "curl "; 60 61 // Construct the expected command 62 const SIMPLE_BASE = [CMD + quote(HTTPS_SIMPLE_SJS)]; 63 const SLOW_BASE = [CMD + quote(HTTPS_SLOW_SJS)]; 64 const BASE_RESULT = [ 65 "--compressed", 66 header("User-Agent: " + navigator.userAgent), 67 header("Accept: */*"), 68 header("Accept-Language: " + navigator.language), 69 header("X-Custom-Header-1: Custom value"), 70 header("X-Custom-Header-2: 8.8.8.8"), 71 header("X-Custom-Header-3: Mon, 3 Mar 2014 11:11:11 GMT"), 72 header("Referer: " + HTTPS_CURL_URL), 73 header("Connection: keep-alive"), 74 header("Pragma: no-cache"), 75 header("Cache-Control: no-cache"), 76 header("Sec-Fetch-Dest: empty"), 77 header("Sec-Fetch-Mode: cors"), 78 header("Sec-Fetch-Site: same-origin"), 79 ]; 80 81 const COOKIE_PARTIAL_RESULT = [header("Cookie: bob=true; tom=cool")]; 82 83 const POST_PARTIAL_RESULT = [ 84 "-X", 85 "POST", 86 "--data-raw " + quote(POST_PAYLOAD), 87 header("Content-Type: text/plain;charset=UTF-8"), 88 ]; 89 const ORIGIN_RESULT = [header("Origin: https://example.com")]; 90 91 const HEAD_PARTIAL_RESULT = ["-I"]; 92 93 return { 94 SIMPLE_BASE, 95 SLOW_BASE, 96 BASE_RESULT, 97 COOKIE_PARTIAL_RESULT, 98 POST_PAYLOAD, 99 POST_PARTIAL_RESULT, 100 ORIGIN_RESULT, 101 HEAD_PARTIAL_RESULT, 102 }; 103 } 104 105 async function testForPlatform(tab, monitor, testData) { 106 // GET request, no cookies (first request) 107 await performRequest("GET"); 108 for (const test of testData) { 109 await testClipboardContent(test.menuItemId, [ 110 ...test.data.SIMPLE_BASE, 111 ...test.data.BASE_RESULT, 112 ]); 113 } 114 // Check to make sure it is still OK after we view the response (bug#1452442) 115 await selectIndexAndWaitForSourceEditor(monitor, 0); 116 for (const test of testData) { 117 await testClipboardContent(test.menuItemId, [ 118 ...test.data.SIMPLE_BASE, 119 ...test.data.BASE_RESULT, 120 ]); 121 } 122 123 // GET request, cookies set by previous response 124 await performRequest("GET"); 125 for (const test of testData) { 126 await testClipboardContent(test.menuItemId, [ 127 ...test.data.SIMPLE_BASE, 128 ...test.data.BASE_RESULT, 129 ...test.data.COOKIE_PARTIAL_RESULT, 130 ]); 131 } 132 133 // Unfinished request (bug#1378464, bug#1420513) 134 const waitSlow = waitForNetworkEvents(monitor, 0); 135 await SpecialPowers.spawn( 136 tab.linkedBrowser, 137 [HTTPS_SLOW_SJS], 138 async function (url) { 139 content.wrappedJSObject.performRequest(url, "GET", null); 140 } 141 ); 142 await waitSlow; 143 for (const test of testData) { 144 await testClipboardContent(test.menuItemId, [ 145 ...test.data.SLOW_BASE, 146 ...test.data.BASE_RESULT, 147 ...test.data.COOKIE_PARTIAL_RESULT, 148 ]); 149 } 150 151 // POST request 152 await performRequest("POST", POST_PAYLOAD); 153 for (const test of testData) { 154 await testClipboardContent(test.menuItemId, [ 155 ...test.data.SIMPLE_BASE, 156 ...test.data.BASE_RESULT, 157 ...test.data.COOKIE_PARTIAL_RESULT, 158 ...test.data.POST_PARTIAL_RESULT, 159 ...test.data.ORIGIN_RESULT, 160 ]); 161 } 162 163 // HEAD request 164 await performRequest("HEAD"); 165 for (const test of testData) { 166 await testClipboardContent(test.menuItemId, [ 167 ...test.data.SIMPLE_BASE, 168 ...test.data.BASE_RESULT, 169 ...test.data.COOKIE_PARTIAL_RESULT, 170 ...test.data.HEAD_PARTIAL_RESULT, 171 ]); 172 } 173 174 async function performRequest(method, payload) { 175 const waitRequest = waitForNetworkEvents(monitor, 1); 176 await SpecialPowers.spawn( 177 tab.linkedBrowser, 178 [ 179 { 180 url: HTTPS_SIMPLE_SJS, 181 method_: method, 182 payload_: payload, 183 }, 184 ], 185 async function ({ url, method_, payload_ }) { 186 content.wrappedJSObject.performRequest(url, method_, payload_); 187 } 188 ); 189 await waitRequest; 190 } 191 192 async function testClipboardContent(menuItemId, expectedResult) { 193 const { document } = monitor.panelWin; 194 195 const items = document.querySelectorAll(".request-list-item"); 196 const itemIndex = items.length - 1; 197 EventUtils.sendMouseEvent({ type: "mousedown" }, items[itemIndex]); 198 EventUtils.sendMouseEvent( 199 { type: "contextmenu" }, 200 document.querySelectorAll(".request-list-item")[0] 201 ); 202 203 /* Ensure that the copy as cURL option is always visible */ 204 is( 205 !!getContextMenuItem(monitor, menuItemId), 206 true, 207 `The "Copy as cURL" context menu item "${menuItemId}" should not be hidden.` 208 ); 209 210 await waitForClipboardPromise( 211 async function setup() { 212 await selectContextMenuItem(monitor, menuItemId); 213 }, 214 function validate(result) { 215 if (typeof result !== "string") { 216 return false; 217 } 218 219 // Different setups may produce the same command, but with the 220 // parameters in a different order in the commandline (which is fine). 221 // Here we confirm that the commands are the same even in that case. 222 223 // This monster regexp parses the command line into an array of arguments, 224 // recognizing quoted args with matching quotes and escaped quotes inside: 225 // [ "curl.exe 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ] 226 // [ "curl 'url'", "--standalone-arg", "-arg-with-quoted-string 'value\'s'" ] 227 const matchRe = /[-\.A-Za-z1-9]+(?: ([\^\"']+)(?:\\\1|.)*?\1)?/g; 228 229 const actual = result.match(matchRe); 230 // Must begin with the same "curl 'URL'" segment 231 if (!actual || expectedResult[0] != actual[0]) { 232 return false; 233 } 234 235 // Must match each of the params in the middle (headers) 236 return ( 237 expectedResult.length === actual.length && 238 expectedResult.some(param => actual.includes(param)) 239 ); 240 } 241 ); 242 243 info( 244 `Clipboard contains a cURL command for item ${itemIndex} by "${menuItemId}"` 245 ); 246 } 247 }