|
| 1 | +from datetime import datetime |
| 2 | + |
| 3 | +# Beyond the 128-byte limit for `Last-Event-ID` |
| 4 | +long_string = b"a" * 255 |
| 5 | + |
| 6 | +# A regular, safe `Last-Event-ID` value |
| 7 | +safe_id_value = b"abc" |
| 8 | + |
| 9 | +# CORS-unsafe request-header byte 0x3C (`<`) in `Last-Event-ID` |
| 10 | +unsafe_id_value = b"e5p3n<3k0k0s" |
| 11 | + |
| 12 | +def main(request, response): |
| 13 | + origin = request.headers.get(b"Origin") |
| 14 | + cors_request_headers = request.headers.get(b"Access-Control-Request-Headers") |
| 15 | + |
| 16 | + # Allow any CORS origin |
| 17 | + if origin is not None: |
| 18 | + response.headers.set(b"Access-Control-Allow-Origin", origin) |
| 19 | + |
| 20 | + # Allow any CORS request headers |
| 21 | + if cors_request_headers is not None: |
| 22 | + response.headers.set(b"Access-Control-Allow-Headers", cors_request_headers) |
| 23 | + |
| 24 | + # Expect a `token` in the query string |
| 25 | + if b"token" not in request.GET: |
| 26 | + headers = [(b"Content-Type", b"text/plain")] |
| 27 | + return 400, headers, b"ERROR: `token` query parameter!" |
| 28 | + |
| 29 | + # Expect a `fixture` in the query string |
| 30 | + if b"fixture" not in request.GET: |
| 31 | + headers = [(b"Content-Type", b"text/plain")] |
| 32 | + return 400, headers, b"ERROR: `fixture` query parameter!" |
| 33 | + |
| 34 | + # Prepare state |
| 35 | + fixture = request.GET.first(b"fixture") |
| 36 | + token = request.GET.first(b"token") |
| 37 | + last_event_id = request.headers.get(b"Last-Event-ID", b"") |
| 38 | + expect_preflight = fixture == b"unsafe" or fixture == b"long" |
| 39 | + |
| 40 | + # Preflight handling |
| 41 | + if request.method == u"OPTIONS": |
| 42 | + # The first request (without any `Last-Event-ID` header) should _never_ be a |
| 43 | + # preflight request, since it should be considered a "safe" request. |
| 44 | + # If we _do_ send a preflight for these requests, error early. |
| 45 | + if last_event_id == b"": |
| 46 | + headers = [(b"Content-Type", b"text/plain")] |
| 47 | + return 400, headers, b"ERROR: No Last-Event-ID header in preflight!" |
| 48 | + |
| 49 | + # We keep track of the different "tokens" we see, in order to tell whether or not |
| 50 | + # a client has done a preflight request. If the "stash" does not contain a token, |
| 51 | + # no preflight request was made. |
| 52 | + request.server.stash.put(token, cors_request_headers) |
| 53 | + |
| 54 | + # We can return with an empty body on preflight requests |
| 55 | + return b"" |
| 56 | + |
| 57 | + # This will be a SSE endpoint |
| 58 | + response.headers.set(b"Content-Type", b"text/event-stream") |
| 59 | + response.headers.set(b"Cache-Control", b"no-store") |
| 60 | + |
| 61 | + # If we do not have a `Last-Event-ID` header, we're on the initial request |
| 62 | + # Respond with the fixture corresponding to the `fixture` query parameter |
| 63 | + if last_event_id == b"": |
| 64 | + if fixture == b"safe": |
| 65 | + return b"id: " + safe_id_value + b"\nretry: 200\ndata: safe\n\n" |
| 66 | + if fixture == b"unsafe": |
| 67 | + return b"id: " + unsafe_id_value + b"\nretry: 200\ndata: unsafe\n\n" |
| 68 | + if fixture == b"long": |
| 69 | + return b"id: " + long_string + b"\nretry: 200\ndata: long\n\n" |
| 70 | + return b"event: failure\ndata: unknown fixture\n\n" |
| 71 | + |
| 72 | + # If we have a `Last-Event-ID` header, we're on a reconnect. |
| 73 | + # If fixture is "unsafe", eg requires a preflight, check to see that we got one. |
| 74 | + preflight_headers = request.server.stash.take(token) |
| 75 | + saw_preflight = preflight_headers is not None |
| 76 | + if saw_preflight and not expect_preflight: |
| 77 | + return b"event: failure\ndata: saw preflight, did not expect one\n\n" |
| 78 | + elif not saw_preflight and expect_preflight: |
| 79 | + return b"event: failure\ndata: expected preflight, did not get one\n\n" |
| 80 | + |
| 81 | + if saw_preflight and preflight_headers.lower() != b"last-event-id": |
| 82 | + data = b"preflight `access-control-request-headers` was not `last-event-id`" |
| 83 | + return b"event: failure\ndata: " + data + b"\n\n" |
| 84 | + |
| 85 | + # Expect to have the same ID in the header as the one we sent. |
| 86 | + expected = b"<unknown>" |
| 87 | + if fixture == b"safe": |
| 88 | + expected = safe_id_value |
| 89 | + elif fixture == b"unsafe": |
| 90 | + expected = unsafe_id_value |
| 91 | + elif fixture == b"long": |
| 92 | + expected = long_string |
| 93 | + |
| 94 | + event = last_event_id == expected and b"success" or b"failure" |
| 95 | + data = b"got " + last_event_id + b", expected " + expected |
| 96 | + return b"event: " + event + b"\ndata: " + data + b"\n\n" |
0 commit comments