Skip to content

Commit 93392e6

Browse files
committed
feat(binding-http): Make Access-Control-Allow-Origin header configurable
Add allowedOrigins option to HttpConfig interface to let users configure the Access-Control-Allow-Origin header value. Defaults to '*' for backward compatibility. Secured things still echo the request origin with credentials regardless of this setting. Fixes #941 Signed-off-by: jona42-ui <jonathanthembo123@gmail.com>
1 parent 20c336a commit 93392e6

12 files changed

Lines changed: 299 additions & 17 deletions

File tree

packages/binding-http/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ The protocol binding can be configured using his constructor or trough servient
180180
baseUri?: string // A Base URI to be used in the TD in cases where the client will access a different URL than the actual machine serving the thing. [See Using BaseUri below]
181181
urlRewrite?: Record<string, string> // A record to allow for other URLs pointing to existing endpoints, e.g., { "/myroot/myUrl": "/test/properties/test" }
182182
middleware?: MiddlewareRequestHandler; // the MiddlewareRequestHandler function. See [Adding a middleware] section below.
183+
allowedOrigins?: string; // Configures the Access-Control-Allow-Origin header. Defaults to "*" (any origin). See [Configuring CORS] section below.
183184
}
184185
```
185186

@@ -305,6 +306,21 @@ The exposed thing on the internal server will product form URLs such as:
305306

306307
> `address` tells the HttpServer a specific local network interface to bind its TCP listener.
307308

309+
### Configuring CORS
310+
311+
By default, the HTTP binding sets the `Access-Control-Allow-Origin` header to `"*"`, allowing any origin to access exposed Things. You can restrict this to a specific origin using the `allowedOrigins` configuration option:
312+
313+
```js
314+
servient.addServer(
315+
new HttpServer({
316+
port: 8080,
317+
allowedOrigins: "https://my-app.example.com",
318+
})
319+
);
320+
```
321+
322+
When a security scheme (e.g. `basic`, `bearer`) is configured, the server echoes the request's `Origin` header and sets `Access-Control-Allow-Credentials: true`, regardless of the `allowedOrigins` value. This is required for browsers to send credentials in cross-origin requests.
323+
308324
### Adding a middleware
309325

310326
HttpServer supports the addition of **middleware** to handle the raw HTTP requests before they hit the Servient. In the middleware function, you can run some logic to filter and eventually reject HTTP requests (e.g. based on some custom headers).

packages/binding-http/src/http-server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default class HttpServer implements ProtocolServer {
6565
private readonly baseUri?: string;
6666
private readonly urlRewrite?: Record<string, string>;
6767
private readonly devFriendlyUri: boolean;
68+
private readonly allowedOrigins: string;
6869
private readonly supportedSecuritySchemes: string[] = ["nosec"];
6970
private readonly validOAuthClients: RegExp = /.*/g;
7071
private readonly server: http.Server | https.Server;
@@ -85,6 +86,7 @@ export default class HttpServer implements ProtocolServer {
8586
this.urlRewrite = config.urlRewrite;
8687
this.middleware = config.middleware;
8788
this.devFriendlyUri = config.devFriendlyUri ?? true;
89+
this.allowedOrigins = config.allowedOrigins ?? "*";
8890

8991
const router = Router({
9092
ignoreTrailingSlash: true,
@@ -251,6 +253,10 @@ export default class HttpServer implements ProtocolServer {
251253
return this.things;
252254
}
253255

256+
public getAllowedOrigins(): string {
257+
return this.allowedOrigins;
258+
}
259+
254260
/** returns server port number and indicates that server is running when larger than -1 */
255261
public getPort(): number {
256262
const address = this.server?.address();

packages/binding-http/src/http.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ export interface HttpConfig {
4747
security?: SecurityScheme[];
4848
devFriendlyUri?: boolean;
4949
middleware?: MiddlewareRequestHandler;
50+
/**
51+
* Configures the Access-Control-Allow-Origin header.
52+
* Default is "*" (any origin allowed).
53+
*/
54+
allowedOrigins?: string;
5055
}
5156

5257
export interface OAuth2ServerConfig extends SecurityScheme {

packages/binding-http/src/routes/action.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export default async function actionRoute(
6767
return;
6868
}
6969
// TODO: refactor this part to move into a common place
70-
setCorsForThing(req, res, thing);
70+
setCorsForThing(req, res, thing, this.getAllowedOrigins());
7171
let corsPreflightWithCredentials = false;
7272
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
7373

@@ -110,6 +110,6 @@ export default async function actionRoute(
110110
} else {
111111
// may have been OPTIONS that failed the credentials check
112112
// as a result, we pass corsPreflightWithCredentials
113-
respondUnallowedMethod(req, res, "POST", corsPreflightWithCredentials);
113+
respondUnallowedMethod(req, res, "POST", corsPreflightWithCredentials, this.getAllowedOrigins());
114114
}
115115
}

packages/binding-http/src/routes/common.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export function respondUnallowedMethod(
2121
req: IncomingMessage,
2222
res: ServerResponse,
2323
allowed: string,
24-
corsPreflightWithCredentials = false
24+
corsPreflightWithCredentials = false,
25+
allowedOrigins = "*"
2526
): void {
2627
// Always allow OPTIONS to handle CORS pre-flight requests
2728
if (!allowed.includes("OPTIONS")) {
@@ -40,7 +41,7 @@ export function respondUnallowedMethod(
4041
res.setHeader("Access-Control-Allow-Origin", origin);
4142
res.setHeader("Access-Control-Allow-Credentials", "true");
4243
} else {
43-
res.setHeader("Access-Control-Allow-Origin", "*");
44+
res.setHeader("Access-Control-Allow-Origin", allowedOrigins);
4445
}
4546
res.setHeader("Access-Control-Allow-Methods", allowed);
4647
res.setHeader("Access-Control-Allow-Headers", "content-type, authorization, *");
@@ -91,7 +92,12 @@ export function securitySchemeToHttpHeader(scheme: string): string {
9192
return first.toUpperCase() + rest.join("").toLowerCase();
9293
}
9394

94-
export function setCorsForThing(req: IncomingMessage, res: ServerResponse, thing: ExposedThing): void {
95+
export function setCorsForThing(
96+
req: IncomingMessage,
97+
res: ServerResponse,
98+
thing: ExposedThing,
99+
allowedOrigins = "*"
100+
): void {
95101
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
96102
// Set CORS headers
97103

@@ -100,6 +106,6 @@ export function setCorsForThing(req: IncomingMessage, res: ServerResponse, thing
100106
res.setHeader("Access-Control-Allow-Origin", origin);
101107
res.setHeader("Access-Control-Allow-Credentials", "true");
102108
} else {
103-
res.setHeader("Access-Control-Allow-Origin", "*");
109+
res.setHeader("Access-Control-Allow-Origin", allowedOrigins);
104110
}
105111
}

packages/binding-http/src/routes/event.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default async function eventRoute(
4747
return;
4848
}
4949
// TODO: refactor this part to move into a common place
50-
setCorsForThing(req, res, thing);
50+
setCorsForThing(req, res, thing, this.getAllowedOrigins());
5151
let corsPreflightWithCredentials = false;
5252
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
5353

@@ -109,7 +109,7 @@ export default async function eventRoute(
109109
} else {
110110
// may have been OPTIONS that failed the credentials check
111111
// as a result, we pass corsPreflightWithCredentials
112-
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
112+
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials, this.getAllowedOrigins());
113113
}
114114
// resource found and response sent
115115
}

packages/binding-http/src/routes/properties.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default async function propertiesRoute(
3939
}
4040

4141
// TODO: refactor this part to move into a common place
42-
setCorsForThing(req, res, thing);
42+
setCorsForThing(req, res, thing, this.getAllowedOrigins());
4343
let corsPreflightWithCredentials = false;
4444
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
4545

@@ -86,6 +86,6 @@ export default async function propertiesRoute(
8686
} else {
8787
// may have been OPTIONS that failed the credentials check
8888
// as a result, we pass corsPreflightWithCredentials
89-
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
89+
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials, this.getAllowedOrigins());
9090
}
9191
}

packages/binding-http/src/routes/property-observe.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export default async function propertyObserveRoute(
5757
}
5858

5959
// TODO: refactor this part to move into a common place
60-
setCorsForThing(req, res, thing);
60+
setCorsForThing(req, res, thing, this.getAllowedOrigins());
6161
let corsPreflightWithCredentials = false;
6262
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
6363

@@ -113,6 +113,6 @@ export default async function propertyObserveRoute(
113113
res.writeHead(202);
114114
res.end();
115115
} else {
116-
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
116+
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials, this.getAllowedOrigins());
117117
}
118118
}

packages/binding-http/src/routes/property.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export default async function propertyRoute(
7777
}
7878

7979
// TODO: refactor this part to move into a common place
80-
setCorsForThing(req, res, thing);
80+
setCorsForThing(req, res, thing, this.getAllowedOrigins());
8181
let corsPreflightWithCredentials = false;
8282
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
8383

@@ -108,7 +108,7 @@ export default async function propertyRoute(
108108
} else if (req.method === "PUT") {
109109
const readOnly: boolean = property.readOnly ?? false;
110110
if (readOnly) {
111-
respondUnallowedMethod(req, res, "GET, PUT");
111+
respondUnallowedMethod(req, res, "GET, PUT", false, this.getAllowedOrigins());
112112
return;
113113
}
114114

@@ -128,6 +128,6 @@ export default async function propertyRoute(
128128
} else {
129129
// may have been OPTIONS that failed the credentials check
130130
// as a result, we pass corsPreflightWithCredentials
131-
respondUnallowedMethod(req, res, "GET, PUT", corsPreflightWithCredentials);
131+
respondUnallowedMethod(req, res, "GET, PUT", corsPreflightWithCredentials, this.getAllowedOrigins());
132132
} // Property exists?
133133
}

packages/binding-http/src/routes/thing-description.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export default async function thingDescriptionRoute(
169169
const payload = await content.toBuffer();
170170

171171
negotiateLanguage(td, thing, req);
172-
res.setHeader("Access-Control-Allow-Origin", "*");
172+
res.setHeader("Access-Control-Allow-Origin", this.getAllowedOrigins());
173173
res.setHeader("Content-Type", contentType);
174174
res.writeHead(200);
175175
debug(`Sending HTTP response for TD with Content-Type ${contentType}.`);

0 commit comments

Comments
 (0)