diff --git a/cors/cors-safelisted-request-header.any.js b/cors/cors-safelisted-request-header.any.js index a0a0417d74b3db..05ff22576c5a6d 100644 --- a/cors/cors-safelisted-request-header.any.js +++ b/cors/cors-safelisted-request-header.any.js @@ -69,3 +69,12 @@ function safelist(headers, expectPreflight = false) { ].forEach(([value, preflight = false]) => { safelist({"range": value}, preflight); }); + +[ + ["abc123"], + ["\"", true], + ["e5p3n<3k0k0s", true], + ["012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", true], +].forEach(([value, preflight = false]) => { + safelist({"last-event-id": value}, preflight); +}); diff --git a/eventsource/eventsource-cross-origin-preflight.window.js b/eventsource/eventsource-cross-origin-preflight.window.js new file mode 100644 index 00000000000000..8af219b7952249 --- /dev/null +++ b/eventsource/eventsource-cross-origin-preflight.window.js @@ -0,0 +1,45 @@ +// META: title=EventSource: cross-origin preflight +// META: script=/common/utils.js + +const crossdomain = location.href + .replace("://", "://élève.") + .replace(/\/[^\/]*$/, "/"); +const origin = location.origin.replace("://", "://xn--lve-6lad."); + +[ + ["safe `last-event-id` (no preflight)", "safe"], + ["unsafe `last-event-id` (preflight because too long)", "long"], + ["unsafe `last-event-id` (preflight because unsafe characters)", "unsafe"], +].forEach(([name, fixture]) => { + async_test(document.title + " - " + name).step(function () { + const uuid = token(); + const url = + crossdomain + + "resources/cors-unsafe-last-event-id.py?fixture=" + + fixture + + "&token=" + + uuid; + + const source = new EventSource(url); + + // Make sure to close the EventSource after the test is done. + this.add_cleanup(() => source.close()); + + // 1. Event will be a `message` with `id` set to a CORS-safe value, then disconnects. + source.addEventListener( + "message", + this.step_func((evt) => assert_equals(evt.data, fixture)) + ); + + // 2. Will emit either `success` or `failure` event. We expect `success`, + // which is the case if `last-event-id` is set to the same value as received above, + // and a preflight request has been sent for the unsafe `last-event-id` headers. + source.addEventListener("success", this.step_func_done()); + source.addEventListener( + "failure", + this.step_func_done((evt) => { + assert_unreached(evt.data); + }) + ); + }); +}); diff --git a/eventsource/resources/cors-unsafe-last-event-id.py b/eventsource/resources/cors-unsafe-last-event-id.py new file mode 100644 index 00000000000000..01b5aacefbfd4d --- /dev/null +++ b/eventsource/resources/cors-unsafe-last-event-id.py @@ -0,0 +1,89 @@ +from datetime import datetime + +# Beyond the 128-byte limit for `Last-Event-ID` +long_string = b"a" * 255 + +# A regular, safe `Last-Event-ID` value +safe_id_value = b"abc" + +# CORS-unsafe request-header byte 0x3C (`<`) in `Last-Event-ID` +unsafe_id_value = b"e5p3n<3k0k0s" + +def main(request, response): + origin = request.headers.get(b"Origin") + cors_request_headers = request.headers.get(b"Access-Control-Request-Headers") + + # Allow any CORS origin + if origin is not None: + response.headers.set(b"Access-Control-Allow-Origin", origin) + + # Allow any CORS request headers + if cors_request_headers is not None: + response.headers.set(b"Access-Control-Allow-Headers", cors_request_headers) + + # Expect a `token` in the query string + if b"token" not in request.GET: + headers = [(b"Content-Type", b"text/plain")] + return 400, headers, b"ERROR: `token` query parameter!" + + # Expect a `fixture` in the query string + if b"fixture" not in request.GET: + headers = [(b"Content-Type", b"text/plain")] + return 400, headers, b"ERROR: `fixture` query parameter!" + + # Prepare state + fixture = request.GET.first(b"fixture") + token = request.GET.first(b"token") + last_event_id = request.headers.get(b"Last-Event-ID", b"") + expect_preflight = fixture == b"unsafe" or fixture == b"long" + + # Preflight handling + if request.method == u"OPTIONS": + # We keep track of the different "tokens" we see, in order to tell whether or not + # a client has done a preflight request. If the "stash" does not contain a token, + # no preflight request was made. + request.server.stash.put(token, cors_request_headers) + + # We can return with an empty body on preflight requests + return b"" + + # This will be a SSE endpoint + response.headers.set(b"Content-Type", b"text/event-stream") + response.headers.set(b"Cache-Control", b"no-store") + + # If we do not have a `Last-Event-ID` header, we're on the initial request + # Respond with the fixture corresponding to the `fixture` query parameter + if last_event_id == b"": + if fixture == b"safe": + return b"id: " + safe_id_value + b"\nretry: 200\ndata: safe\n\n" + if fixture == b"unsafe": + return b"id: " + unsafe_id_value + b"\nretry: 200\ndata: unsafe\n\n" + if fixture == b"long": + return b"id: " + long_string + b"\nretry: 200\ndata: long\n\n" + return b"event: failure\ndata: unknown fixture\n\n" + + # If we have a `Last-Event-ID` header, we're on a reconnect. + # If fixture is "unsafe", eg requires a preflight, check to see that we got one. + preflight_headers = request.server.stash.take(token) + saw_preflight = preflight_headers is not None + if saw_preflight and not expect_preflight: + return b"event: failure\ndata: saw preflight, did not expect one\n\n" + elif not saw_preflight and expect_preflight: + return b"event: failure\ndata: expected preflight, did not get one\n\n" + + if saw_preflight and preflight_headers.lower() != b"last-event-id": + data = b"preflight `access-control-request-headers` was not `last-event-id`" + return b"event: failure\ndata: " + data + b"\n\n" + + # Expect to have the same ID in the header as the one we sent. + expected = b"" + if fixture == b"safe": + expected = safe_id_value + elif fixture == b"unsafe": + expected = unsafe_id_value + elif fixture == b"long": + expected = long_string + + event = last_event_id == expected and b"success" or b"failure" + data = b"got " + last_event_id + b", expected " + expected + return b"event: " + event + b"\ndata: " + data + b"\n\n" diff --git a/fetch/api/cors/cors-no-preflight.any.js b/fetch/api/cors/cors-no-preflight.any.js index 7a0269aae4ec3d..e3063e75a18ad7 100644 --- a/fetch/api/cors/cors-no-preflight.any.js +++ b/fetch/api/cors/cors-no-preflight.any.js @@ -39,3 +39,5 @@ corsNoPreflight("Cross domain [GET] [Content-Type: multipart/form-data]", host_i corsNoPreflight("Cross domain [GET] [Content-Type: text/plain]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain"); corsNoPreflight("Cross domain [GET] [Content-Type: text/plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "text/plain;charset=utf-8"); corsNoPreflight("Cross domain [GET] [Content-Type: Text/Plain;charset=utf-8]", host_info.HTTP_REMOTE_ORIGIN, "GET" , "Content-Type", "Text/Plain;charset=utf-8"); +corsNoPreflight("Cross domain [GET] [Last-Event-ID: evt-14]", host_info.HTTP_REMOTE_ORIGIN, "GET", "Last-Event-ID", "evt-14"); +corsNoPreflight("Cross domain [GET] [Last-Event-ID: EvT-15]", host_info.HTTP_REMOTE_ORIGIN, "GET", "Last-Event-ID", "EvT-15"); diff --git a/fetch/api/cors/resources/not-cors-safelisted.json b/fetch/api/cors/resources/not-cors-safelisted.json index 945dc0f93ba4a3..4bee8548b62d75 100644 --- a/fetch/api/cors/resources/not-cors-safelisted.json +++ b/fetch/api/cors/resources/not-cors-safelisted.json @@ -8,6 +8,8 @@ ["content-language", "@"], ["content-type", "text/html"], ["content-type", "text/plain; long=0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901"], + ["last-event-id", "\""], + ["last-event-id", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678"], ["range", "bytes 0-"], ["test", "hi"] ] diff --git a/xhr/access-control-basic-cors-safelisted-request-headers.htm b/xhr/access-control-basic-cors-safelisted-request-headers.htm index 56870493b4ed9d..27ac4a3c9adbda 100644 --- a/xhr/access-control-basic-cors-safelisted-request-headers.htm +++ b/xhr/access-control-basic-cors-safelisted-request-headers.htm @@ -17,6 +17,7 @@ xhr.setRequestHeader("Accept-Language", "ru"); xhr.setRequestHeader("Content-Language", "ru"); xhr.setRequestHeader("Content-Type", "text/plain"); + xhr.setRequestHeader("Last-Event-ID", "abc123"); xhr.send(); @@ -24,7 +25,8 @@ "Accept: *\n" + "Accept-Language: ru\n" + "Content-Language: ru\n" + - "Content-Type: text/plain\n"); + "Content-Type: text/plain\n" + + "Last-Event-ID: abc123\n"); }, "Request with CORS-safelisted headers"); diff --git a/xhr/access-control-basic-non-cors-safelisted-last-event-id.htm b/xhr/access-control-basic-non-cors-safelisted-last-event-id.htm new file mode 100644 index 00000000000000..bb77ee13d58d10 --- /dev/null +++ b/xhr/access-control-basic-non-cors-safelisted-last-event-id.htm @@ -0,0 +1,30 @@ + + + + Tests cross-origin request with non-CORS-safelisted last-event-id header + + + + + + + + diff --git a/xhr/resources/access-control-basic-cors-safelisted-request-headers.py b/xhr/resources/access-control-basic-cors-safelisted-request-headers.py index 46523a905a9f70..946e44e4fa68b5 100644 --- a/xhr/resources/access-control-basic-cors-safelisted-request-headers.py +++ b/xhr/resources/access-control-basic-cors-safelisted-request-headers.py @@ -11,6 +11,6 @@ def main(request, response): response.headers.set(b"Access-Control-Allow-Credentials", b"true") response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"origin")) - for header in [b"Accept", b"Accept-Language", b"Content-Language", b"Content-Type"]: + for header in [b"Accept", b"Accept-Language", b"Content-Language", b"Content-Type", b"Last-Event-ID"]: value = request.headers.get(header) response.content += isomorphic_decode(header) + u": " + (isomorphic_decode(value) if value else u"") + u'\n'