Skip to content

Commit ae5e344

Browse files
committed
feat(signalling): add CORS support to web server (#387)
Adds an optional CORS middleware to WebServer so browser-based clients hosted on a different origin can call the REST API and other routes. Wilbur exposes --cors plus --cors_allowed_origins, --cors_allowed_methods, --cors_allowed_headers, and --cors_credentials, and reads matching keys from config.json. Default is off; when on, an empty origin list allows any origin.
1 parent 4cd72bc commit ae5e344

6 files changed

Lines changed: 154 additions & 0 deletions

File tree

.changeset/cors-signalling-api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@epicgames-ps/lib-pixelstreamingsignalling-ue5.7": minor
3+
"@epicgames-ps/wilbur": minor
4+
---
5+
6+
Add CORS support to the signalling web server. A new `IWebServerConfig.cors` option registers the `cors` Express middleware before the rate limiter and any route handlers, so that custom frontends hosted on a different origin can call the REST API (`--rest_api`) or any other route mounted on the app. Wilbur exposes this through a `--cors` CLI flag (default off) plus `--cors_allowed_origins`, `--cors_allowed_methods`, `--cors_allowed_headers`, and `--cors_credentials`. All four sub-options accept comma-separated values and read matching `cors*` keys from `config.json`. When `--cors` is set without an explicit origin list, all origins are allowed.

Signalling/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"license": "MIT",
2121
"devDependencies": {
2222
"@eslint/js": "^9.20.0",
23+
"@types/cors": "^2.8.17",
2324
"@types/express": "^5.0.0",
2425
"@types/node": "^22.14.0",
2526
"@types/ws": "^8.5.14",
@@ -36,6 +37,7 @@
3637
},
3738
"dependencies": {
3839
"body-parser": "^1.20.4",
40+
"cors": "^2.8.5",
3941
"express": "^4.22.1",
4042
"express-rate-limit": "^8.2.2",
4143
"helmet": "^8.0.0",

Signalling/src/WebServer.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,40 @@ import fs from 'fs';
55
import http from 'http';
66
import https from 'https';
77
import helmet from 'helmet';
8+
import cors from 'cors';
89
import { Logger } from './Logger';
910
import RateLimit from 'express-rate-limit';
1011

1112
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1213
const hsts = require('hsts');
1314

15+
/**
16+
* Options for configuring CORS on the Express app. When `enabled` is true the
17+
* `cors` middleware is registered so that browser-based clients hosted on a
18+
* different origin (e.g. a custom frontend) can call the REST API or other
19+
* routes mounted on the app. Use `allowedOrigins` to restrict which origins
20+
* are accepted; an empty / unset list means "allow all origins" (`*`).
21+
*/
22+
export interface ICorsConfig {
23+
// If false (or unset) no CORS headers are added. Defaults to false.
24+
enabled?: boolean;
25+
26+
// List of origins to allow. If empty/unset, all origins are allowed.
27+
allowedOrigins?: string[];
28+
29+
// List of HTTP methods to allow. Defaults to the `cors` package default
30+
// (`GET,HEAD,PUT,PATCH,POST,DELETE`).
31+
allowedMethods?: string[];
32+
33+
// List of request headers to allow. If unset, the `cors` package mirrors
34+
// the request's `Access-Control-Request-Headers` header.
35+
allowedHeaders?: string[];
36+
37+
// Whether the request can include credentials (cookies, auth headers).
38+
// Defaults to false.
39+
credentials?: boolean;
40+
}
41+
1442
/**
1543
* An interface that describes the possible options to pass to
1644
* WebServer.
@@ -45,6 +73,11 @@ export interface IWebServerConfig {
4573
// registered by other subsystems, such as a REST API, remain reachable),
4674
// but no static files are served.
4775
serveStatic?: boolean;
76+
77+
// Optional CORS configuration. When `cors.enabled` is true the cors
78+
// middleware is registered before the rate limiter and any route handlers,
79+
// so that all routes mounted on the app respond with CORS headers.
80+
cors?: ICorsConfig;
4881
}
4982

5083
/**
@@ -117,6 +150,33 @@ export class WebServer {
117150
}
118151
}
119152

153+
if (config.cors?.enabled) {
154+
const corsOptions: cors.CorsOptions = {};
155+
156+
if (config.cors.allowedOrigins && config.cors.allowedOrigins.length > 0) {
157+
corsOptions.origin = config.cors.allowedOrigins;
158+
}
159+
if (config.cors.allowedMethods && config.cors.allowedMethods.length > 0) {
160+
corsOptions.methods = config.cors.allowedMethods;
161+
}
162+
if (config.cors.allowedHeaders && config.cors.allowedHeaders.length > 0) {
163+
corsOptions.allowedHeaders = config.cors.allowedHeaders;
164+
}
165+
if (config.cors.credentials) {
166+
corsOptions.credentials = true;
167+
}
168+
169+
// Register cors before the rate limiter so that preflight (OPTIONS)
170+
// requests are answered with CORS headers even when an origin is
171+
// close to the rate limit, and before any route handlers so the
172+
// homepage, static files, and the REST API all respond uniformly.
173+
app.use(cors(corsOptions));
174+
175+
Logger.info(
176+
`CORS enabled. Allowed origins: ${corsOptions.origin ? JSON.stringify(corsOptions.origin) : '*'}`
177+
);
178+
}
179+
120180
const limiter = RateLimit({
121181
windowMs: 60 * 1000, // 1 minute
122182
max: config.perMinuteRateLimit ? config.perMinuteRateLimit : 3000

SignallingWebServer/config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
"ssl_cert_path": "certificates/client-cert.pem",
1515
"https_redirect": true,
1616
"rest_api": false,
17+
"cors": false,
18+
"cors_allowed_origins": "",
19+
"cors_allowed_methods": "",
20+
"cors_allowed_headers": "",
21+
"cors_credentials": false,
1722
"reverse_proxy": false,
1823
"reverse_proxy_num_proxies": 1,
1924
"peer_options": "",

SignallingWebServer/src/index.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,31 @@ program
145145
'Enables the rest API interface that can be accessed at <server_url>/api/api-definition',
146146
config_file.rest_api || false
147147
)
148+
.option(
149+
'--cors',
150+
'Enables CORS headers on the webserver. Required for browser-based clients hosted on a different origin to call routes such as the REST API.',
151+
config_file.cors || false
152+
)
153+
.option(
154+
'--cors_allowed_origins <origins>',
155+
'Comma-separated list of allowed origins (e.g. "https://example.com,https://other.example.com"). If omitted, all origins are allowed.',
156+
config_file.cors_allowed_origins || ''
157+
)
158+
.option(
159+
'--cors_allowed_methods <methods>',
160+
'Comma-separated list of allowed HTTP methods. Defaults to the cors middleware default if omitted.',
161+
config_file.cors_allowed_methods || ''
162+
)
163+
.option(
164+
'--cors_allowed_headers <headers>',
165+
"Comma-separated list of allowed request headers. If omitted, the request's Access-Control-Request-Headers value is mirrored.",
166+
config_file.cors_allowed_headers || ''
167+
)
168+
.option(
169+
'--cors_credentials',
170+
'Allow credentials (cookies, authorization headers) on cross-origin requests.',
171+
config_file.cors_credentials || false
172+
)
148173
.addOption(
149174
new Option(
150175
'--peer_options <json-string>',
@@ -257,6 +282,24 @@ if (shouldServerStart) {
257282
serveStatic: options.serve
258283
};
259284

285+
if (options.cors) {
286+
const splitCsv = (value: unknown): string[] => {
287+
if (typeof value !== 'string' || value.length === 0) return [];
288+
return value
289+
.split(',')
290+
.map((s) => s.trim())
291+
.filter((s) => s.length > 0);
292+
};
293+
294+
webserverOptions.cors = {
295+
enabled: true,
296+
allowedOrigins: splitCsv(options.cors_allowed_origins),
297+
allowedMethods: splitCsv(options.cors_allowed_methods),
298+
allowedHeaders: splitCsv(options.cors_allowed_headers),
299+
credentials: !!options.cors_credentials
300+
};
301+
}
302+
260303
if (options.serve) {
261304
Logger.info('Static file serving enabled.');
262305
} else if (options.rest_api) {

package-lock.json

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)