support.sub.js (7571B)
1 // Maps protocol (without the trailing colon) and address space to port. 2 // 3 // TODO(crbug.com/418737577): change keys to be consistent with new address 4 // space names. 5 const SERVER_PORTS = { 6 "http": { 7 "loopback": {{ports[http][0]}}, 8 "other-loopback": {{ports[http][1]}}, 9 "local": {{ports[http-local][0]}}, 10 "public": {{ports[http-public][0]}}, 11 }, 12 "https": { 13 "loopback": {{ports[https][0]}}, 14 "other-loopback": {{ports[https][1]}}, 15 "local": {{ports[https-local][0]}}, 16 "public": {{ports[https-public][0]}}, 17 }, 18 "ws": { 19 "loopback": {{ports[ws][0]}}, 20 }, 21 "wss": { 22 "loopback": {{ports[wss][0]}}, 23 }, 24 }; 25 26 // A `Server` is a web server accessible by tests. It has the following shape: 27 // 28 // { 29 // addressSpace: the IP address space of the server ("loopback", "local" or 30 // "public"), 31 // name: a human-readable name for the server, 32 // port: the port on which the server listens for connections, 33 // protocol: the protocol (including trailing colon) spoken by the server, 34 // } 35 // 36 // Constants below define the available servers, which can also be accessed 37 // programmatically with `get()`. 38 class Server { 39 // Maps the given `protocol` (without a trailing colon) and `addressSpace` to 40 // a server. Returns null if no such server exists. 41 static get(protocol, addressSpace) { 42 const ports = SERVER_PORTS[protocol]; 43 if (ports === undefined) { 44 return null; 45 } 46 47 const port = ports[addressSpace]; 48 if (port === undefined) { 49 return null; 50 } 51 52 return { 53 addressSpace, 54 name: `${protocol}-${addressSpace}`, 55 port, 56 protocol: protocol + ':', 57 }; 58 } 59 60 static HTTP_LOOPBACK = Server.get("http", "loopback"); 61 static OTHER_HTTP_LOOPBACK = Server.get("http", "other-loopback"); 62 static HTTP_LOCAL = Server.get("http", "local"); 63 static HTTP_PUBLIC = Server.get("http", "public"); 64 static HTTPS_LOOPBACK = Server.get("https", "loopback"); 65 static OTHER_HTTPS_LOOPBACK = Server.get("https", "other-loopback"); 66 static HTTPS_LOCAL = Server.get("https", "local"); 67 static HTTPS_PUBLIC = Server.get("https", "public"); 68 static WS_LOOPBACK = Server.get("ws", "loopback"); 69 static WSS_LOOPBACK = Server.get("wss", "loopback"); 70 }; 71 72 // Resolves a URL relative to the current location, returning an absolute URL. 73 // 74 // `url` specifies the relative URL, e.g. "foo.html" or "http://foo.example". 75 // `options`, if defined, should have the following shape: 76 // 77 // { 78 // // Optional. Overrides the protocol of the returned URL. 79 // protocol, 80 // 81 // // Optional. Overrides the port of the returned URL. 82 // port, 83 // 84 // // Extra headers. 85 // headers, 86 // 87 // // Extra search params. 88 // searchParams, 89 // } 90 // 91 function resolveUrl(url, options) { 92 const result = new URL(url, window.location); 93 if (options === undefined) { 94 return result; 95 } 96 97 const { port, protocol, headers, searchParams } = options; 98 if (port !== undefined) { 99 result.port = port; 100 } 101 if (protocol !== undefined) { 102 result.protocol = protocol; 103 } 104 if (headers !== undefined) { 105 const pipes = []; 106 for (key in headers) { 107 pipes.push(`header(${key},${headers[key]})`); 108 } 109 result.searchParams.append("pipe", pipes.join("|")); 110 } 111 if (searchParams !== undefined) { 112 for (key in searchParams) { 113 result.searchParams.append(key, searchParams[key]); 114 } 115 } 116 117 return result; 118 } 119 120 // Computes options to pass to `resolveUrl()` for a source document's URL. 121 // 122 // `server` identifies the server from which to load the document. 123 // `treatAsPublic`, if set to true, specifies that the source document should 124 // be artificially placed in the `public` address space using CSP. 125 function sourceResolveOptions({ server, treatAsPublic }) { 126 const options = {...server}; 127 if (treatAsPublic) { 128 options.headers = { "Content-Security-Policy": "treat-as-public-address" }; 129 } 130 return options; 131 } 132 133 // Computes the URL of a target handler configured with the given options. 134 // 135 // `server` identifies the server from which to load the resource. 136 // `behavior` specifies the behavior of the target server. It may contain: 137 // - `response`: The result of calling one of `ResponseBehavior`'s methods. 138 // - `redirect`: A URL to which the target should redirect GET requests. 139 function resolveTargetUrl({ server, behavior }) { 140 if (server === undefined) { 141 throw new Error("no server specified."); 142 } 143 const options = {...server}; 144 if (behavior) { 145 const { response, redirect } = behavior; 146 options.searchParams = { 147 ...response, 148 }; 149 if (redirect !== undefined) { 150 options.searchParams.redirect = redirect; 151 } 152 } 153 154 return resolveUrl("target.py", options); 155 } 156 157 // Methods generate behavior specifications for how `resources/target.py` 158 // should behave upon receiving a regular (non-preflight) request. 159 const ResponseBehavior = { 160 // The response should succeed without CORS headers. 161 default: () => ({}), 162 163 // The response should succeed with CORS headers. 164 allowCrossOrigin: () => ({ "final-headers": "cors" }), 165 }; 166 167 const FetchTestResult = { 168 SUCCESS: { 169 ok: true, 170 body: "success", 171 }, 172 OPAQUE: { 173 ok: false, 174 type: "opaque", 175 body: "", 176 }, 177 FAILURE: { 178 error: "TypeError: Failed to fetch", 179 }, 180 }; 181 182 // Helper function for checking results from fetch tests. 183 function checkTestResult(actual, expected) { 184 assert_equals(actual.error, expected.error, "error mismatch"); 185 assert_equals(actual.ok, expected.ok, "response ok mismatch"); 186 assert_equals(actual.body, expected.body, "response body mismatch"); 187 188 if (expected.type !== undefined) { 189 assert_equals(type, expected.type, "response type mismatch"); 190 } 191 } 192 193 // Registers an event listener that will resolve this promise when this 194 // window receives a message posted to it. 195 function futureMessage(options) { 196 return new Promise(resolve => { 197 window.addEventListener('message', (e) => { 198 resolve(e.data); 199 }); 200 }); 201 }; 202 203 const NavigationTestResult = { 204 SUCCESS: 'loaded', 205 FAILURE: 'timeout', 206 }; 207 208 async function iframeTest( 209 t, {source, target, expected, permission = 'denied'}) { 210 const targetUrl = 211 resolveUrl('resources/openee.html', sourceResolveOptions(target)); 212 213 const sourceUrl = 214 resolveUrl('resources/iframer.html', sourceResolveOptions(source)); 215 sourceUrl.searchParams.set('permission', permission); 216 sourceUrl.searchParams.set('url', targetUrl); 217 218 const popup = window.open(sourceUrl); 219 t.add_cleanup(() => popup.close()); 220 221 // The child frame posts a message iff it loads successfully. 222 // There exists no interoperable way to check whether an iframe failed to 223 // load, so we use a timeout. 224 // See: https://github.com/whatwg/html/issues/125 225 const result = await Promise.race([ 226 futureMessage().then((data) => data.message), 227 new Promise((resolve) => { 228 t.step_timeout(() => resolve('timeout'), 2000 /* ms */); 229 }), 230 ]); 231 232 assert_equals(result, expected); 233 } 234 235 async function navigateTest(t, {source, target, expected}) { 236 const targetUrl = 237 resolveUrl('resources/openee.html', sourceResolveOptions(target)); 238 239 const sourceUrl = 240 resolveUrl('resources/navigate.html', sourceResolveOptions(source)); 241 sourceUrl.searchParams.set('url', targetUrl); 242 243 const popup = window.open(sourceUrl); 244 t.add_cleanup(() => popup.close()); 245 246 const result = await Promise.race([ 247 futureMessage().then((data) => data.message), 248 new Promise((resolve) => { 249 t.step_timeout(() => resolve('timeout'), 2000 /* ms */); 250 }), 251 ]); 252 253 assert_equals(result, expected); 254 }