Skip to content

Commit

Permalink
Add tests for CORS-unsafe Last-Event-ID in EventSource
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Nov 20, 2024
1 parent fc84ad4 commit 79c1fed
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 0 deletions.
33 changes: 33 additions & 0 deletions eventsource/eventsource-cross-origin-preflight.window.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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` (too long)', 'long'],
['unsafe `last-event-id` (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(e => assert_equals(e.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', (evt) => {
this.step(() => assert_unreached(evt.data))
this.done()
})
})
})
96 changes: 96 additions & 0 deletions eventsource/resources/cors-unsafe-last-event-id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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":
# The first request (without any `Last-Event-ID` header) should _never_ be a
# preflight request, since it should be considered a "safe" request.
# If we _do_ send a preflight for these requests, error early.
if last_event_id == b"":
headers = [(b"Content-Type", b"text/plain")]
return 400, headers, b"ERROR: No Last-Event-ID header in preflight!"

# 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"<unknown>"
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"

0 comments on commit 79c1fed

Please sign in to comment.