feat: CORS allowlist for cross-origin HTTP clients#82
Conversation
Configurable via a new ALLOWED_ORIGINS setting in
PluginConfiguration. Empty list (the default) preserves the
historical behaviour of never sending CORS headers, so existing
setups are unaffected.
When configured, the server:
- echoes the request's Origin in Access-Control-Allow-Origin
if it matches the allowlist (with Vary: Origin), rather than
emitting a wildcard
- responds 204 to OPTIONS preflight with standard
methods/headers
- leaves non-allowlisted requests untouched, so browsers
block-by-default
Lets dashboards and SPAs hosted on a different origin (e.g.
GitHub Pages, a local Vite dev server) read action responses
without needing a same-origin proxy.
- Match origins case-insensitively (RFC 6454 §6.1) via HashSet<string> with StringComparer.OrdinalIgnoreCase. Browsers normalise to lowercase before sending, but admin entries shouldn't have to. - Always send Vary: Origin when CORS is configured, including on non-matching responses. Otherwise a shared cache can serve a no-CORS response to a subsequent cross-origin request that would have matched. - Reject Origin values containing CR or LF before echoing into the Access-Control-Allow-Origin header. WebSocketSharp's header setter already validates, but defence in depth is cheap and avoids relying on library behaviour. - OPTIONS requests no longer fall through to the data pipeline when CORS is configured (a non-matching preflight previously serialised an empty JSON body); unconfigured-CORS behaviour is unchanged.
|
LGTM on the feature shape. Pushed a follow-up commit (4e839a7) with three small hardening tweaks while it was in front of me — flagging here for the record so the diff doesn't surprise anyone:
Also tightened the OPTIONS handling so a non-matching preflight 204s without serialising an empty JSON body (only when CORS is configured — unconfigured-CORS OPTIONS behaviour is unchanged from before this PR). Merging. |
Summary
Adds a configurable CORS allowlist so JavaScript on origins other than the API host can read Telemachus responses. Today Telemachus sends no CORS headers at all, so anything hosted off-origin (a separate machine on the LAN, a deployed static dashboard, a dev server on a different port) is blocked by the browser even when the user explicitly wants it to work.
Behaviour
ALLOWED_ORIGINSsetting inPluginConfiguration(Telemachus'sPluginData/Telemachus/config.xml).OrigininAccess-Control-Allow-Originif it matches the allowlist, withVary: Origin. Echoing rather than emitting*keeps the same permissiveness for configured clients but blocks credentialed requests from arbitrary origins.Access-Control-Allow-Methods/Access-Control-Allow-Headers/Access-Control-Max-Age.Config example
Comma-separated
scheme://host[:port]entries — match exactly what the browser sends in theOriginheader. Trailing slashes and empty entries are stripped.Validation
Tested locally on KSP 1.12.x with
ALLOWED_ORIGINSset inconfig.xml:Allowed CORS origins: <list>appears inKSP.logon init when the config is set.curl -H "Origin: <allowlisted>" -i http://127.0.0.1:8085/telemachus/datalink?a=v.altitudereturnsAccess-Control-Allow-Origin: <allowlisted>+Vary: Origin.Originreturns no CORS header, and a browser fetch from that origin is blocked as expected.Originreturns 204 with the expectedAccess-Control-*headers.ALLOWED_ORIGINSunset (the default), responses include no CORS headers at all — the bundled same-origin web UI continues to load normally.