summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_xhr_cors.js
blob: 983fe1c5422d732164ba4d771293427324797a4d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"use strict";

// The purpose of this test is to show that the XMLHttpRequest API behaves
// similarly in MV2 and MV3, except for intentional differences related to
// permission handling.

Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);

const server = createHttpServer({
  hosts: ["example.com", "example.net", "example.org"],
});
server.registerPathHandler("/dummy", (req, res) => {
  res.setStatusLine(req.httpVersion, 200, "OK");
  res.setHeader("Content-Type", "text/html; charset=utf-8");

  // A very strict CSP.
  res.setHeader(
    "Content-Security-Policy",
    "default-src; script-src 'nonce-kindasecret'; connect-src http:"
  );

  res.write(
    `<script id="id_of_some_element" nonce="kindasecret">
      // Clobber XMLHttpRequest API to allow us to verify that the page's value
      // for it does not affect the XMLHttpRequest API in the content script.
      window.XMLHttpRequest = "This is not XMLHttpRequest";
      </script>
    `
  );
});
server.registerPathHandler("/dummy.json", (req, res) => {
  res.write(`{"mykey": "kvalue"}`);
});
server.registerPathHandler("/nocors", (req, res) => {
  res.write("no cors");
});
server.registerPathHandler("/cors-enabled", (req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "http://example.com");
  res.write("cors_response");
});
server.registerPathHandler("/return-origin", (req, res) => {
  res.setHeader("Content-Type", "text/plain");
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "*");
  res.write(req.hasHeader("Origin") ? req.getHeader("Origin") : "undefined");
});

// We just need to test XHR; fetch is already covered by test_ext_secfetch.js.
async function test_xhr({ manifest_version }) {
  async function contentScript(manifest_version) {
    function runXHR(url, extraXHRProps, method = "GET") {
      return new Promise(resolve => {
        let x = new XMLHttpRequest();
        x.open(method, url);
        Object.assign(x, extraXHRProps);
        x.onloadend = () => resolve(x);
        x.send();
      });
    }
    async function checkXHR({
      description,
      url,
      extraXHRProps,
      method,
      expected,
    }) {
      let { status, response } = expected;
      let x = await runXHR(url, extraXHRProps, method);
      browser.test.assertEq(status, x.status, `${description} - status`);
      browser.test.assertEq(response, x.response, `${description} - body`);
    }

    await checkXHR({
      description: "Same-origin",
      url: "http://example.com/nocors",
      expected: { status: 200, response: "no cors" },
    });

    await checkXHR({
      description: "Cross-origin without CORS",
      url: "http://example.org/nocors",
      expected: { status: 0, response: "" },
    });

    await checkXHR({
      description: "Cross-origin with CORS",
      url: "http://example.org/cors-enabled",
      expected:
        manifest_version === 2
          ? // Bug 1605197: MV2 cannot fall back to CORS.
            { status: 0, response: "" }
          : { status: 200, response: "cors_response" },
    });

    // MV2 allowed cross-origin requests in content scripts with host
    // permissions, but MV3 does not.
    await checkXHR({
      description: "Cross-origin without CORS, with permission",
      url: "http://example.net/nocors",
      expected:
        manifest_version === 2
          ? { status: 200, response: "no cors" }
          : { status: 0, response: "" },
    });

    await checkXHR({
      description: "Cross-origin with CORS (and permission)",
      url: "http://example.net/cors-enabled",
      expected: { status: 200, response: "cors_response" },
    });

    // MV2 has a XMLHttpRequest instance specific to the sandbox.
    // MV3 uses the page's XMLHttpRequest and currently enforces the page's CSP.
    // TODO bug 1766813: Enforce content script CSP instead.
    await checkXHR({
      description: "data:-URL while page blocks data: via CSP",
      url: "data:,data-url",
      expected:
        // Should be "data-url" in MV3 too.
        manifest_version === 2
          ? { status: 200, response: "data-url" }
          : { status: 0, response: "" },
    });

    {
      let x = await runXHR("http://example.com/dummy.json", {
        responseType: "json",
      });
      browser.test.assertTrue(x.response instanceof Object, "is JSON object");
      browser.test.assertEq(x.response.mykey, "kvalue", "can read parsed JSON");
    }

    {
      let x = await runXHR("http://example.com/dummy", {
        responseType: "document",
      });
      browser.test.assertTrue(HTMLDocument.isInstance(x.response), "is doc");
      browser.test.assertTrue(
        x.response.querySelector("#id_of_some_element"),
        "got parsed document"
      );
    }

    await checkXHR({
      description: "Same-origin Origin header",
      url: "http://example.com/return-origin",
      expected: { status: 200, response: "undefined" },
    });

    await checkXHR({
      description: "Same-origin POST Origin header",
      url: "http://example.com/return-origin",
      method: "POST",
      expected:
        manifest_version === 2
          ? { status: 200, response: "undefined" }
          : { status: 200, response: "http://example.com" },
    });

    await checkXHR({
      description: "Cross-origin (CORS) Origin header",
      url: "http://example.org/return-origin",
      expected:
        manifest_version === 2
          ? // Bug 1605197: MV2 cannot fall back to CORS.
            { status: 0, response: "" }
          : { status: 200, response: "http://example.com" },
    });

    await checkXHR({
      description: "Cross-origin (CORS) POST Origin header",
      url: "http://example.org/return-origin",
      method: "POST",
      expected:
        manifest_version === 2
          ? // Bug 1605197: MV2 cannot fall back to CORS.
            { status: 0, response: "" }
          : { status: 200, response: "http://example.com" },
    });

    browser.test.sendMessage("done");
  }
  let extension = ExtensionTestUtils.loadExtension({
    temporarilyInstalled: true, // Needed for granted_host_permissions
    manifest: {
      manifest_version,
      granted_host_permissions: true, // Test-only: grant permissions in MV3.
      host_permissions: [
        "http://example.net/",
        // Work-around for bug 1766752.
        "http://example.com/",
        // "http://example.org/" is intentionally missing.
      ],
      content_scripts: [
        {
          matches: ["http://example.com/dummy"],
          run_at: "document_end",
          js: ["contentscript.js"],
        },
      ],
    },
    files: {
      "contentscript.js": `(${contentScript})(${manifest_version})`,
    },
  });
  await extension.startup();

  let contentPage = await ExtensionTestUtils.loadContentPage(
    "http://example.com/dummy"
  );
  await extension.awaitMessage("done");
  await contentPage.close();

  await extension.unload();
}

add_task(async function test_XHR_MV2() {
  await test_xhr({ manifest_version: 2 });
});

add_task(async function test_XHR_MV3() {
  await test_xhr({ manifest_version: 3 });
});