diff options
Diffstat (limited to 'dom/tests/mochitest/fetch/test_fetch_cors.js')
-rw-r--r-- | dom/tests/mochitest/fetch/test_fetch_cors.js | 1883 |
1 files changed, 1883 insertions, 0 deletions
diff --git a/dom/tests/mochitest/fetch/test_fetch_cors.js b/dom/tests/mochitest/fetch/test_fetch_cors.js new file mode 100644 index 0000000000..05ce221435 --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_cors.js @@ -0,0 +1,1883 @@ +var path = "/tests/dom/base/test/"; + +function isOpaqueResponse(response) { + return ( + response.type == "opaque" && + response.status === 0 && + response.statusText === "" + ); +} + +function testModeSameOrigin() { + // Fetch spec Section 4, step 4, "request's mode is same-origin". + var req = new Request("http://example.com", { mode: "same-origin" }); + return fetch(req).then( + function (res) { + ok( + false, + "Attempting to fetch a resource from a different origin with mode same-origin should fail." + ); + }, + function (e) { + ok( + e instanceof TypeError, + "Attempting to fetch a resource from a different origin with mode same-origin should fail." + ); + } + ); +} + +function testNoCorsCtor() { + // Request constructor Step 19.1 + var simpleMethods = ["GET", "HEAD", "POST"]; + for (var i = 0; i < simpleMethods.length; ++i) { + var r = new Request("http://example.com", { + method: simpleMethods[i], + mode: "no-cors", + }); + ok( + true, + "no-cors Request with simple method " + simpleMethods[i] + " is allowed." + ); + } + + var otherMethods = ["DELETE", "OPTIONS", "PUT"]; + for (var i = 0; i < otherMethods.length; ++i) { + try { + var r = new Request("http://example.com", { + method: otherMethods[i], + mode: "no-cors", + }); + ok( + false, + "no-cors Request with non-simple method " + + otherMethods[i] + + " is not allowed." + ); + } catch (e) { + ok( + true, + "no-cors Request with non-simple method " + + otherMethods[i] + + " is not allowed." + ); + } + } + + // Request constructor Step 19.2, check guarded headers. + var r = new Request(".", { mode: "no-cors" }); + r.headers.append("Content-Type", "multipart/form-data"); + is( + r.headers.get("content-type"), + "multipart/form-data", + "Appending simple header should succeed" + ); + r.headers.append("custom", "value"); + ok(!r.headers.has("custom"), "Appending custom header should fail"); + r.headers.append("DNT", "value"); + ok(!r.headers.has("DNT"), "Appending forbidden header should fail"); +} + +var corsServerPath = + "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?"; +function testModeNoCors() { + // Fetch spec, section 4, step 4, response tainting should be set opaque, so + // that fetching leads to an opaque filtered response in step 8. + var r = new Request("http://example.com" + corsServerPath + "status=200", { + mode: "no-cors", + }); + return fetch(r).then( + function (res) { + ok( + isOpaqueResponse(res), + "no-cors Request fetch should result in opaque response" + ); + }, + function (e) { + ok(false, "no-cors Request fetch should not error"); + } + ); +} + +function testSameOriginCredentials() { + var cookieStr = "type=chocolatechip"; + var tests = [ + { + // Initialize by setting a cookie. + pass: 1, + setCookie: cookieStr, + withCred: "same-origin", + }, + { + // Default mode is "same-origin". + pass: 1, + cookie: cookieStr, + }, + { + pass: 1, + noCookie: 1, + withCred: "omit", + }, + { + pass: 1, + cookie: cookieStr, + withCred: "same-origin", + }, + { + pass: 1, + cookie: cookieStr, + withCred: "include", + }, + ]; + + var finalPromiseResolve, finalPromiseReject; + var finalPromise = new Promise(function (res, rej) { + finalPromiseResolve = res; + finalPromiseReject = rej; + }); + + function makeRequest(test) { + req = { + // Add a default query param just to make formatting the actual params + // easier. + url: corsServerPath + "a=b", + method: test.method, + headers: test.headers, + withCred: test.withCred, + }; + + if (test.setCookie) { + req.url += "&setCookie=" + escape(test.setCookie); + } + if (test.cookie) { + req.url += "&cookie=" + escape(test.cookie); + } + if (test.noCookie) { + req.url += "&noCookie"; + } + + return new Request(req.url, { + method: req.method, + headers: req.headers, + credentials: req.withCred, + }); + } + + function testResponse(res, test) { + ok(test.pass, "Expected test to pass " + JSON.stringify(test)); + is(res.status, 200, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test)); + return res.text().then(function (v) { + is( + v, + "<res>hello pass</res>\n", + "wrong text in test for " + JSON.stringify(test) + ); + }); + } + + function runATest(tests, i) { + var test = tests[i]; + var request = makeRequest(test); + console.log(request.url); + fetch(request).then( + function (res) { + testResponse(res, test).then(function () { + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + }); + }, + function (e) { + ok(!test.pass, "Expected test to fail " + JSON.stringify(test)); + ok(e instanceof TypeError, "Test should fail " + JSON.stringify(test)); + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + } + ); + } + + runATest(tests, 0); + return finalPromise; +} + +function testModeCors() { + var tests = [ + // Plain request + { pass: 1, method: "GET", noAllowPreflight: 1 }, + + // undefined username + { pass: 1, method: "GET", noAllowPreflight: 1, username: undefined }, + + // undefined username and password + { + pass: 1, + method: "GET", + noAllowPreflight: 1, + username: undefined, + password: undefined, + }, + + // nonempty username + { pass: 0, method: "GET", noAllowPreflight: 1, username: "user" }, + + // nonempty password + { pass: 0, method: "GET", noAllowPreflight: 1, password: "password" }, + + // Default allowed headers + { + pass: 1, + method: "GET", + headers: { + "Content-Type": "text/plain", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + + { + pass: 0, + method: "GET", + headers: { + "Content-Type": "foo/bar", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + + { + pass: 0, + method: "GET", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + + { + pass: 0, + method: "GET", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // Custom headers + { + pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "X-My-Header", + }, + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header": + "secondValue", + }, + allowHeaders: + "x-my-header, long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header", + }, + { + pass: 1, + method: "GET", + headers: { "x-my%-header": "myValue" }, + allowHeaders: "x-my%-header", + }, + { pass: 0, method: "GET", headers: { "x-my-header": "myValue" } }, + { pass: 0, method: "GET", headers: { "x-my-header": "" } }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "y-my-header", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header y-my-header", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, y-my-header z", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, y-my-he(ader", + }, + { + pass: 0, + method: "GET", + headers: { myheader: "" }, + allowMethods: "myheader", + }, + { + pass: 1, + method: "GET", + headers: { "User-Agent": "myValue" }, + allowHeaders: "User-Agent", + }, + { pass: 0, method: "GET", headers: { "User-Agent": "myValue" } }, + + // Multiple custom headers + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue", + }, + allowHeaders: "x-my-header, second-header, third-header", + }, + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue", + }, + allowHeaders: "x-my-header,second-header,third-header", + }, + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue", + }, + allowHeaders: "x-my-header ,second-header ,third-header", + }, + { + pass: 1, + method: "GET", + headers: { + "x-my-header": "myValue", + "second-header": "secondValue", + "third-header": "thirdValue", + }, + allowHeaders: "x-my-header , second-header , third-header", + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", "second-header": "secondValue" }, + allowHeaders: ", x-my-header, , ,, second-header, , ", + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "myValue", "second-header": "secondValue" }, + allowHeaders: "x-my-header, second-header, unused-header", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "myValue", "y-my-header": "secondValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "", "y-my-header": "" }, + allowHeaders: "x-my-header", + }, + + // HEAD requests + { pass: 1, method: "HEAD", noAllowPreflight: 1 }, + + // HEAD with safe headers + { + pass: 1, + method: "HEAD", + headers: { + "Content-Type": "text/plain", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "HEAD", + headers: { + "Content-Type": "foo/bar", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "HEAD", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "HEAD", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // HEAD with custom headers + { + pass: 1, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { pass: 0, method: "HEAD", headers: { "x-my-header": "myValue" } }, + { + pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "", + }, + { + pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "y-my-header", + }, + { + pass: 0, + method: "HEAD", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header y-my-header", + }, + + // POST tests + { pass: 1, method: "POST", body: "hi there", noAllowPreflight: 1 }, + { pass: 1, method: "POST" }, + { pass: 1, method: "POST", noAllowPreflight: 1 }, + + // POST with standard headers + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + noAllowPreflight: 1, + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "multipart/form-data" }, + noAllowPreflight: 1, + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + }, + { pass: 0, method: "POST", headers: { "Content-Type": "foo/bar" } }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { + "Content-Type": "text/plain", + Accept: "foo/bar", + "Accept-Language": "sv-SE", + }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar, text/plain" }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar, text/plain, garbage" }, + noAllowPreflight: 1, + }, + + // POST with custom headers + { + pass: 1, + method: "POST", + body: "hi there", + headers: { + Accept: "foo/bar", + "Accept-Language": "sv-SE", + "x-my-header": "myValue", + }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + headers: { "Content-Type": "text/plain", "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar", "x-my-header": "myValue" }, + allowHeaders: "x-my-header, content-type", + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + noAllowPreflight: 1, + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar", "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header", + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "x-my-header": "myValue" }, + allowHeaders: "x-my-header, $_%", + }, + + // Other methods + { pass: 1, method: "DELETE", allowMethods: "DELETE" }, + { pass: 0, method: "DELETE", allowHeaders: "DELETE" }, + { pass: 0, method: "DELETE" }, + { pass: 0, method: "DELETE", allowMethods: "" }, + { pass: 1, method: "DELETE", allowMethods: "POST, PUT, DELETE" }, + { pass: 1, method: "DELETE", allowMethods: "POST, DELETE, PUT" }, + { pass: 1, method: "DELETE", allowMethods: "DELETE, POST, PUT" }, + { pass: 1, method: "DELETE", allowMethods: "POST ,PUT ,DELETE" }, + { pass: 1, method: "DELETE", allowMethods: "POST,PUT,DELETE" }, + { pass: 1, method: "DELETE", allowMethods: "POST , PUT , DELETE" }, + { + pass: 1, + method: "DELETE", + allowMethods: " ,, PUT ,, , , DELETE , ,", + }, + { pass: 0, method: "DELETE", allowMethods: "PUT" }, + { pass: 0, method: "DELETE", allowMethods: "DELETEZ" }, + { pass: 0, method: "DELETE", allowMethods: "DELETE PUT" }, + { pass: 0, method: "DELETE", allowMethods: "DELETE, PUT Z" }, + { pass: 0, method: "DELETE", allowMethods: "DELETE, PU(T" }, + { pass: 0, method: "DELETE", allowMethods: "PUT DELETE" }, + { pass: 0, method: "DELETE", allowMethods: "PUT Z, DELETE" }, + { pass: 0, method: "DELETE", allowMethods: "PU(T, DELETE" }, + { pass: 0, method: "PUT", allowMethods: "put" }, + + // Status messages + { + pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 404, + statusMessage: "nothin' here", + }, + { + pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 401, + statusMessage: "no can do", + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "foo/bar" }, + allowHeaders: "content-type", + status: 500, + statusMessage: "server boo", + }, + { + pass: 1, + method: "GET", + noAllowPreflight: 1, + status: 200, + statusMessage: "Yes!!", + }, + { + pass: 0, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 400, + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 200, + }, + { + pass: 1, + method: "GET", + headers: { "x-my-header": "header value" }, + allowHeaders: "x-my-header", + preflightStatus: 204, + }, + + // exposed headers + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header", + expectedResponseHeaders: ["x-my-header"], + }, + { + pass: 0, + method: "GET", + origin: "http://invalid", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header y", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "y x-my-header", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header, y-my-header z", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header" }, + exposeHeaders: "x-my-header, y-my-hea(er", + expectedResponseHeaders: [], + }, + { + pass: 1, + method: "GET", + responseHeaders: { "x-my-header": "x header", "y-my-header": "y header" }, + exposeHeaders: " , ,,y-my-header,z-my-header, ", + expectedResponseHeaders: ["y-my-header"], + }, + { + pass: 1, + method: "GET", + responseHeaders: { + "Cache-Control": "cacheControl header", + "Content-Language": "contentLanguage header", + Expires: "expires header", + "Last-Modified": "lastModified header", + Pragma: "pragma header", + Unexpected: "unexpected header", + }, + expectedResponseHeaders: [ + "Cache-Control", + "Content-Language", + "Content-Type", + "Expires", + "Last-Modified", + "Pragma", + ], + }, + // Check that sending a body in the OPTIONS response works + { + pass: 1, + method: "DELETE", + allowMethods: "DELETE", + preflightBody: "I'm a preflight response body", + }, + ]; + + var baseURL = "http://example.org" + corsServerPath; + var origin = "http://mochi.test:8888"; + var fetches = []; + for (test of tests) { + var req = { + url: baseURL + "allowOrigin=" + escape(test.origin || origin), + method: test.method, + headers: test.headers, + uploadProgress: test.uploadProgress, + body: test.body, + responseHeaders: test.responseHeaders, + }; + + if (test.pass) { + req.url += "&origin=" + escape(origin) + "&requestMethod=" + test.method; + } + + if ("username" in test) { + var u = new URL(req.url); + u.username = test.username || ""; + req.url = u.href; + } + + if ("password" in test) { + var u = new URL(req.url); + u.password = test.password || ""; + req.url = u.href; + } + + if (test.noAllowPreflight) { + req.url += "&noAllowPreflight"; + } + + if (test.pass && "headers" in test) { + function isUnsafeHeader(name) { + lName = name.toLowerCase(); + return ( + lName != "accept" && + lName != "accept-language" && + (lName != "content-type" || + ![ + "text/plain", + "multipart/form-data", + "application/x-www-form-urlencoded", + ].includes(test.headers[name].toLowerCase())) + ); + } + req.url += "&headers=" + escape(JSON.stringify(test.headers)); + reqHeaders = escape( + Object.keys(test.headers) + .filter(isUnsafeHeader) + .map(s => s.toLowerCase()) + .sort() + .join(",") + ); + req.url += reqHeaders ? "&requestHeaders=" + reqHeaders : ""; + } + if ("allowHeaders" in test) { + req.url += "&allowHeaders=" + escape(test.allowHeaders); + } + if ("allowMethods" in test) { + req.url += "&allowMethods=" + escape(test.allowMethods); + } + if (test.body) { + req.url += "&body=" + escape(test.body); + } + if (test.status) { + req.url += "&status=" + test.status; + req.url += "&statusMessage=" + escape(test.statusMessage); + } + if (test.preflightStatus) { + req.url += "&preflightStatus=" + test.preflightStatus; + } + if (test.responseHeaders) { + req.url += + "&responseHeaders=" + escape(JSON.stringify(test.responseHeaders)); + } + if (test.exposeHeaders) { + req.url += "&exposeHeaders=" + escape(test.exposeHeaders); + } + if (test.preflightBody) { + req.url += "&preflightBody=" + escape(test.preflightBody); + } + + fetches.push( + (function (test) { + return new Promise(function (resolve) { + resolve( + new Request(req.url, { + method: req.method, + mode: "cors", + headers: req.headers, + body: req.body, + }) + ); + }) + .then(function (request) { + return fetch(request); + }) + .then(function (res) { + ok(test.pass, "Expected test to pass for " + JSON.stringify(test)); + if (test.status) { + is( + res.status, + test.status, + "wrong status in test for " + JSON.stringify(test) + ); + is( + res.statusText, + test.statusMessage, + "wrong status text for " + JSON.stringify(test) + ); + } else { + is( + res.status, + 200, + "wrong status in test for " + JSON.stringify(test) + ); + is( + res.statusText, + "OK", + "wrong status text for " + JSON.stringify(test) + ); + } + if (test.responseHeaders) { + for (header in test.responseHeaders) { + if (!test.expectedResponseHeaders.includes(header)) { + is( + res.headers.has(header), + false, + "|Headers.has()|wrong response header (" + + header + + ") in test for " + + JSON.stringify(test) + ); + } else { + is( + res.headers.get(header), + test.responseHeaders[header], + "|Headers.get()|wrong response header (" + + header + + ") in test for " + + JSON.stringify(test) + ); + } + } + } + + return res.text(); + }) + .then(function (v) { + if (test.method !== "HEAD") { + is( + v, + "<res>hello pass</res>\n", + "wrong responseText in test for " + JSON.stringify(test) + ); + } else { + is( + v, + "", + "wrong responseText in HEAD test for " + JSON.stringify(test) + ); + } + }) + .catch(function (e) { + ok(!test.pass, "Expected test failure for " + JSON.stringify(test)); + ok( + e instanceof TypeError, + "Exception should be TypeError for " + JSON.stringify(test) + ); + }); + })(test) + ); + } + + return Promise.all(fetches); +} + +function testCrossOriginCredentials() { + var origin = "http://mochi.test:8888"; + var tests = [ + { pass: 1, method: "GET", withCred: "include", allowCred: 1 }, + { pass: 0, method: "GET", withCred: "include", allowCred: 0 }, + { pass: 0, method: "GET", withCred: "include", allowCred: 1, origin: "*" }, + { pass: 1, method: "GET", withCred: "omit", allowCred: 1, origin: "*" }, + { + pass: 1, + method: "GET", + setCookie: "a=1", + withCred: "include", + allowCred: 1, + }, + { + pass: 1, + method: "GET", + cookie: "a=1", + withCred: "include", + allowCred: 1, + }, + { pass: 1, method: "GET", noCookie: 1, withCred: "omit", allowCred: 1 }, + { pass: 0, method: "GET", noCookie: 1, withCred: "include", allowCred: 1 }, + { + pass: 1, + method: "GET", + setCookie: "a=2", + withCred: "omit", + allowCred: 1, + }, + { + pass: 1, + method: "GET", + cookie: "a=1", + withCred: "include", + allowCred: 1, + }, + { + pass: 1, + method: "GET", + setCookie: "a=2", + withCred: "include", + allowCred: 1, + }, + { + pass: 1, + method: "GET", + cookie: "a=2", + withCred: "include", + allowCred: 1, + }, + { + // When credentials mode is same-origin, but mode is cors, no + // cookie should be sent cross origin. + pass: 0, + method: "GET", + cookie: "a=2", + withCred: "same-origin", + allowCred: 1, + }, + { + // When credentials mode is same-origin, but mode is cors, no + // cookie should be sent cross origin. This test checks the same + // thing as above, but uses the noCookie check on the server + // instead, and expects a valid response. + pass: 1, + method: "GET", + noCookie: 1, + withCred: "same-origin", + }, + { + // Initialize by setting a cookies for same- and cross- origins. + pass: 1, + hops: [ + { server: origin, setCookie: escape("a=1") }, + { + server: "http://example.com", + allowOrigin: origin, + allowCred: 1, + setCookie: escape("a=2"), + }, + ], + withCred: "include", + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { server: "http://example.com", allowOrigin: origin, noCookie: 1 }, + ], + withCred: "same-origin", + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { + server: "http://example.com", + allowOrigin: origin, + allowCred: 1, + cookie: escape("a=2"), + }, + ], + withCred: "include", + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { server: "http://example.com", allowOrigin: "*", noCookie: 1 }, + ], + withCred: "same-origin", + }, + { + pass: 0, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { + server: "http://example.com", + allowOrigin: "*", + allowCred: 1, + cookie: escape("a=2"), + }, + ], + withCred: "include", + }, + // fails because allow-credentials CORS header is not set by server + { + pass: 0, + method: "GET", + hops: [ + { server: origin, cookie: escape("a=1") }, + { server: origin, cookie: escape("a=1") }, + { + server: "http://example.com", + allowOrigin: origin, + cookie: escape("a=2"), + }, + ], + withCred: "include", + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin, noCookie: 1 }, + { server: origin, noCookie: 1 }, + { server: "http://example.com", allowOrigin: origin, noCookie: 1 }, + ], + withCred: "omit", + }, + ]; + + var baseURL = "http://example.org" + corsServerPath; + var origin = "http://mochi.test:8888"; + + var finalPromiseResolve, finalPromiseReject; + var finalPromise = new Promise(function (res, rej) { + finalPromiseResolve = res; + finalPromiseReject = rej; + }); + + function makeRequest(test) { + var url; + if (test.hops) { + url = + test.hops[0].server + + corsServerPath + + "hop=1&hops=" + + escape(JSON.stringify(test.hops)); + } else { + url = baseURL + "allowOrigin=" + escape(test.origin || origin); + } + req = { + url, + method: test.method, + headers: test.headers, + withCred: test.withCred, + }; + + if (test.allowCred) { + req.url += "&allowCred"; + } + + if (test.setCookie) { + req.url += "&setCookie=" + escape(test.setCookie); + } + if (test.cookie) { + req.url += "&cookie=" + escape(test.cookie); + } + if (test.noCookie) { + req.url += "&noCookie"; + } + + if ("allowHeaders" in test) { + req.url += "&allowHeaders=" + escape(test.allowHeaders); + } + if ("allowMethods" in test) { + req.url += "&allowMethods=" + escape(test.allowMethods); + } + + return new Request(req.url, { + method: req.method, + headers: req.headers, + credentials: req.withCred, + }); + } + + function testResponse(res, test) { + ok(test.pass, "Expected test to pass for " + JSON.stringify(test)); + is(res.status, 200, "wrong status in test for " + JSON.stringify(test)); + is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test)); + return res.text().then(function (v) { + is( + v, + "<res>hello pass</res>\n", + "wrong text in test for " + JSON.stringify(test) + ); + }); + } + + function runATest(tests, i) { + var test = tests[i]; + var request = makeRequest(test); + fetch(request).then( + function (res) { + testResponse(res, test).then(function () { + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + }); + }, + function (e) { + ok(!test.pass, "Expected test failure for " + JSON.stringify(test)); + ok( + e instanceof TypeError, + "Exception should be TypeError for " + JSON.stringify(test) + ); + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + } + ); + } + + runATest(tests, 0); + return finalPromise; +} + +function testModeNoCorsCredentials() { + var cookieStr = "type=chocolatechip"; + var tests = [ + { + // Initialize by setting a cookie. + pass: 1, + setCookie: cookieStr, + withCred: "include", + }, + { + pass: 1, + noCookie: 1, + withCred: "omit", + }, + { + pass: 1, + noCookie: 1, + withCred: "same-origin", + }, + { + pass: 1, + cookie: cookieStr, + withCred: "include", + }, + { + pass: 1, + cookie: cookieStr, + withCred: "omit", + status: 500, + }, + { + pass: 1, + cookie: cookieStr, + withCred: "same-origin", + status: 500, + }, + { + pass: 1, + noCookie: 1, + withCred: "include", + status: 500, + }, + ]; + + var finalPromiseResolve, finalPromiseReject; + var finalPromise = new Promise(function (res, rej) { + finalPromiseResolve = res; + finalPromiseReject = rej; + }); + + function makeRequest(test) { + req = { + url: "http://example.org" + corsServerPath + "a+b", + withCred: test.withCred, + }; + + if (test.setCookie) { + req.url += "&setCookie=" + escape(test.setCookie); + } + if (test.cookie) { + req.url += "&cookie=" + escape(test.cookie); + } + if (test.noCookie) { + req.url += "&noCookie"; + } + + return new Request(req.url, { + method: "GET", + mode: "no-cors", + credentials: req.withCred, + }); + } + + function testResponse(res, test) { + is(res.type, "opaque", "wrong response type for " + JSON.stringify(test)); + + // Get unfiltered response + var chromeResponse = SpecialPowers.wrap(res); + var unfiltered = chromeResponse.cloneUnfiltered(); + + var status = test.status ? test.status : 200; + is( + unfiltered.status, + status, + "wrong status in test for " + JSON.stringify(test) + ); + + return unfiltered.text().then(function (v) { + if (test.status === 200) { + const expected = + SpecialPowers.getIntPref( + "browser.opaqueResponseBlocking.filterFetchResponse" + ) > 0 + ? "" + : "<res>hello pass</res>\n"; + is(v, expected, "wrong text in test for " + JSON.stringify(test)); + } + }); + } + + function runATest(tests, i) { + if (typeof SpecialPowers !== "object") { + finalPromiseResolve(); + return; + } + + var test = tests[i]; + var request = makeRequest(test); + fetch(request).then( + function (res) { + ok(test.pass, "Expected test to pass " + JSON.stringify(test)); + testResponse(res, test).then(function () { + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + }); + }, + function (e) { + ok(!test.pass, "Expected test to fail " + JSON.stringify(test)); + ok(e instanceof TypeError, "Test should fail " + JSON.stringify(test)); + if (i < tests.length - 1) { + runATest(tests, i + 1); + } else { + finalPromiseResolve(); + } + } + ); + } + + runATest(tests, 0); + return finalPromise; +} + +function testCORSRedirects() { + var origin = "http://mochi.test:8888"; + + var tests = [ + { + pass: 1, + method: "GET", + hops: [{ server: "http://example.com", allowOrigin: origin }], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 1, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://mochi.test:8888", allowOrigin: "*" }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://mochi.test:8888" }, + ], + }, + { + pass: 1, + method: "GET", + hops: [ + { server: "http://mochi.test:8888" }, + { server: "http://mochi.test:8888" }, + { server: "http://example.com", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://mochi.test:8888" }, + { server: "http://mochi.test:8888" }, + { server: "http://example.com", allowOrigin: origin }, + { server: "http://mochi.test:8888" }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { + server: "http://sub2.xn--lt-uia.mochi.test:8888", + allowOrigin: origin, + }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: "*" }, + ], + }, + { + pass: 1, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: "*" }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "x" }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "GET", + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://test2.mochi.test:8888", allowOrigin: origin }, + { server: "http://sub2.xn--lt-uia.mochi.test:8888", allowOrigin: "*" }, + { server: "http://sub1.test1.mochi.test:8888" }, + ], + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + hops: [ + { server: "http://mochi.test:8888" }, + { server: "http://example.com", allowOrigin: origin }, + ], + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + noAllowPreflight: 1, + }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://test1.example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + { + server: "http://test2.example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { + pass: 1, + method: "DELETE", + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { + pass: 0, + method: "DELETE", + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + noAllowPreflight: 1, + }, + ], + }, + { + pass: 0, + method: "DELETE", + hops: [ + { server: "http://mochi.test:8888" }, + { + server: "http://test1.example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + { + server: "http://test2.example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://example.com", allowOrigin: origin }, + { server: "http://sub1.test1.mochi.test:8888", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "DELETE", + hops: [ + { + server: "http://example.com", + allowOrigin: origin, + allowMethods: "DELETE", + }, + { + server: "http://sub1.test1.mochi.test:8888", + allowOrigin: origin, + allowMethods: "DELETE", + }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { server: "http://example.com" }, + { + server: "http://sub1.test1.mochi.test:8888", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + { + pass: 1, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain" }, + hops: [ + { server: "http://mochi.test:8888" }, + { server: "http://example.com", allowOrigin: origin }, + ], + }, + { + pass: 0, + method: "POST", + body: "hi there", + headers: { "Content-Type": "text/plain", "my-header": "myValue" }, + hops: [ + { + server: "http://example.com", + allowOrigin: origin, + allowHeaders: "my-header", + }, + { + server: "http://mochi.test:8888", + allowOrigin: origin, + allowHeaders: "my-header", + }, + ], + }, + ]; + + var fetches = []; + for (test of tests) { + req = { + url: + test.hops[0].server + + corsServerPath + + "hop=1&hops=" + + escape(JSON.stringify(test.hops)), + method: test.method, + headers: test.headers, + body: test.body, + }; + + if (test.headers) { + req.url += "&headers=" + escape(JSON.stringify(test.headers)); + } + + if (test.pass) { + if (test.body) { + req.url += "&body=" + escape(test.body); + } + } + + var request = new Request(req.url, { + method: req.method, + headers: req.headers, + body: req.body, + }); + fetches.push( + (function (request, test) { + return fetch(request).then( + function (res) { + ok(test.pass, "Expected test to pass for " + JSON.stringify(test)); + is( + res.status, + 200, + "wrong status in test for " + JSON.stringify(test) + ); + is( + res.statusText, + "OK", + "wrong status text for " + JSON.stringify(test) + ); + is( + res.type, + "cors", + "wrong response type for " + JSON.stringify(test) + ); + var reqHost = new URL(req.url).host; + // If there is a service worker present, the redirections will be + // transparent, assuming that the original request is to the current + // site and would be intercepted. + if (isSWPresent) { + if (reqHost === location.host) { + is( + new URL(res.url).host, + reqHost, + "Response URL should be original URL with a SW present" + ); + } + } else { + is( + new URL(res.url).host, + new URL(test.hops[test.hops.length - 1].server).host, + "Response URL should be redirected URL" + ); + } + return res.text().then(function (v) { + is( + v, + "<res>hello pass</res>\n", + "wrong responseText in test for " + JSON.stringify(test) + ); + }); + }, + function (e) { + ok(!test.pass, "Expected test failure for " + JSON.stringify(test)); + ok( + e instanceof TypeError, + "Exception should be TypeError for " + JSON.stringify(test) + ); + } + ); + })(request, test) + ); + } + + return Promise.all(fetches); +} + +function testNoCORSRedirects() { + var origin = "http://mochi.test:8888"; + + var tests = [ + { pass: 1, method: "GET", hops: [{ server: "http://example.com" }] }, + { + pass: 1, + method: "GET", + hops: [{ server: origin }, { server: "http://example.com" }], + }, + { + pass: 1, + method: "GET", + // Must use a simple header due to no-cors header restrictions. + headers: { "accept-language": "en-us" }, + hops: [{ server: origin }, { server: "http://example.com" }], + }, + { + pass: 1, + method: "GET", + hops: [ + { server: origin }, + { server: "http://example.com" }, + { server: origin }, + ], + }, + { + pass: 1, + method: "POST", + body: "upload body here", + hops: [{ server: origin }, { server: "http://example.com" }], + }, + { + pass: 0, + method: "DELETE", + hops: [{ server: origin }, { server: "http://example.com" }], + }, + ]; + + var fetches = []; + for (test of tests) { + req = { + url: + test.hops[0].server + + corsServerPath + + "hop=1&hops=" + + escape(JSON.stringify(test.hops)), + method: test.method, + headers: test.headers, + body: test.body, + }; + + if (test.headers) { + req.url += "&headers=" + escape(JSON.stringify(test.headers)); + } + + if (test.pass) { + if (test.body) { + req.url += "&body=" + escape(test.body); + } + } + + fetches.push( + (function (req, test) { + return new Promise(function (resolve, reject) { + resolve( + new Request(req.url, { + mode: "no-cors", + method: req.method, + headers: req.headers, + body: req.body, + }) + ); + }) + .then(function (request) { + return fetch(request); + }) + .then( + function (res) { + ok( + test.pass, + "Expected test to pass for " + JSON.stringify(test) + ); + // All requests are cross-origin no-cors, we should always have + // an opaque response here. All values on the opaque response + // should be hidden. + is( + res.type, + "opaque", + "wrong response type for " + JSON.stringify(test) + ); + is( + res.status, + 0, + "wrong status in test for " + JSON.stringify(test) + ); + is( + res.statusText, + "", + "wrong status text for " + JSON.stringify(test) + ); + is(res.url, "", "wrong response url for " + JSON.stringify(test)); + return res.text().then(function (v) { + is( + v, + "", + "wrong responseText in test for " + JSON.stringify(test) + ); + }); + }, + function (e) { + ok( + !test.pass, + "Expected test failure for " + JSON.stringify(test) + ); + ok( + e instanceof TypeError, + "Exception should be TypeError for " + JSON.stringify(test) + ); + } + ); + })(req, test) + ); + } + + return Promise.all(fetches); +} + +function testReferrer() { + var referrer; + if (self && self.location) { + referrer = self.location.href; + } else { + referrer = document.documentURI; + } + + var dict = { + Referer: referrer, + }; + return fetch( + corsServerPath + "headers=" + encodeURIComponent(JSON.stringify(dict)) + ).then( + function (res) { + is(res.status, 200, "expected correct referrer header to be sent"); + dump(res.statusText); + }, + function (e) { + ok(false, "expected correct referrer header to be sent"); + } + ); +} + +function runTest() { + testNoCorsCtor(); + let promise = Promise.resolve(); + if (typeof SpecialPowers === "object") { + promise = SpecialPowers.pushPrefEnv({ + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + set: [["network.cookie.sameSite.laxByDefault", false]], + }); + } + + return promise + .then(testModeSameOrigin) + .then(testModeNoCors) + .then(testModeCors) + .then(testSameOriginCredentials) + .then(testCrossOriginCredentials) + .then(testModeNoCorsCredentials) + .then(testCORSRedirects) + .then(testNoCORSRedirects) + .then(testReferrer); + // Put more promise based tests here. +} |