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"
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 @@ + + +
+
+ + + + +
+