Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/binding-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ The protocol binding can be configured using his constructor or trough servient
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]
urlRewrite?: Record<string, string> // A record to allow for other URLs pointing to existing endpoints, e.g., { "/myroot/myUrl": "/test/properties/test" }
middleware?: MiddlewareRequestHandler; // the MiddlewareRequestHandler function. See [Adding a middleware] section below.
allowedOrigins?: string; // Configures the Access-Control-Allow-Origin header. Defaults to "*" (any origin). See [Configuring CORS] section below.
}
```

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

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

### Configuring CORS

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:

```js
servient.addServer(
new HttpServer({
port: 8080,
allowedOrigins: "https://my-app.example.com",
})
);
```

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.

### Adding a middleware

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).
Expand Down
6 changes: 6 additions & 0 deletions packages/binding-http/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default class HttpServer implements ProtocolServer {
private readonly baseUri?: string;
private readonly urlRewrite?: Record<string, string>;
private readonly devFriendlyUri: boolean;
private readonly allowedOrigins: string;
private readonly supportedSecuritySchemes: string[] = ["nosec"];
private readonly validOAuthClients: RegExp = /.*/g;
private readonly server: http.Server | https.Server;
Expand All @@ -85,6 +86,7 @@ export default class HttpServer implements ProtocolServer {
this.urlRewrite = config.urlRewrite;
this.middleware = config.middleware;
this.devFriendlyUri = config.devFriendlyUri ?? true;
this.allowedOrigins = config.allowedOrigins ?? "*";

const router = Router({
ignoreTrailingSlash: true,
Expand Down Expand Up @@ -251,6 +253,10 @@ export default class HttpServer implements ProtocolServer {
return this.things;
}

public getAllowedOrigins(): string {
return this.allowedOrigins;
}

/** returns server port number and indicates that server is running when larger than -1 */
public getPort(): number {
const address = this.server?.address();
Expand Down
5 changes: 5 additions & 0 deletions packages/binding-http/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export interface HttpConfig {
security?: SecurityScheme[];
devFriendlyUri?: boolean;
middleware?: MiddlewareRequestHandler;
/**
* Configures the Access-Control-Allow-Origin header.
* Default is "*" (any origin allowed).
Comment on lines +51 to +52
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new allowedOrigins configuration option should be documented in the README.md file. Consider adding documentation in the HttpServer Configuration section (around line 271-280) to show users how to configure this option. Example:

new HttpServer({
    port: 8080,
    allowedOrigins: "https://my-app.example.com" // Restrict CORS to specific origin (default: "*")
})

This would help users understand how to use this feature and what the default behavior is.

Suggested change
* Configures the Access-Control-Allow-Origin header.
* Default is "*" (any origin allowed).
* Configures the Access-Control-Allow-Origin header used for CORS.
*
* Default: "*" (any origin allowed).
*
* Example:
* ```ts
* new HttpServer({
* port: 8080,
* allowedOrigins: "https://my-app.example.com" // restrict CORS to specific origin
* });
* ```

Copilot uses AI. Check for mistakes.
*/
allowedOrigins?: string;
}

export interface OAuth2ServerConfig extends SecurityScheme {
Expand Down
4 changes: 2 additions & 2 deletions packages/binding-http/src/routes/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default async function actionRoute(
return;
}
// TODO: refactor this part to move into a common place
setCorsForThing(req, res, thing);
setCorsForThing(req, res, thing, this.getAllowedOrigins());
let corsPreflightWithCredentials = false;
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;

Expand Down Expand Up @@ -110,6 +110,6 @@ export default async function actionRoute(
} else {
// may have been OPTIONS that failed the credentials check
// as a result, we pass corsPreflightWithCredentials
respondUnallowedMethod(req, res, "POST", corsPreflightWithCredentials);
respondUnallowedMethod(req, res, "POST", corsPreflightWithCredentials, this.getAllowedOrigins());
}
}
14 changes: 10 additions & 4 deletions packages/binding-http/src/routes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export function respondUnallowedMethod(
req: IncomingMessage,
res: ServerResponse,
allowed: string,
corsPreflightWithCredentials = false
corsPreflightWithCredentials = false,
allowedOrigins = "*"
): void {
// Always allow OPTIONS to handle CORS pre-flight requests
if (!allowed.includes("OPTIONS")) {
Expand All @@ -40,7 +41,7 @@ export function respondUnallowedMethod(
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
} else {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Origin", allowedOrigins);
}
res.setHeader("Access-Control-Allow-Methods", allowed);
res.setHeader("Access-Control-Allow-Headers", "content-type, authorization, *");
Expand Down Expand Up @@ -91,7 +92,12 @@ export function securitySchemeToHttpHeader(scheme: string): string {
return first.toUpperCase() + rest.join("").toLowerCase();
}

export function setCorsForThing(req: IncomingMessage, res: ServerResponse, thing: ExposedThing): void {
export function setCorsForThing(
req: IncomingMessage,
res: ServerResponse,
thing: ExposedThing,
allowedOrigins = "*"
): void {
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;
// Set CORS headers

Expand All @@ -100,6 +106,6 @@ export function setCorsForThing(req: IncomingMessage, res: ServerResponse, thing
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
} else {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Origin", allowedOrigins);
}
}
4 changes: 2 additions & 2 deletions packages/binding-http/src/routes/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default async function eventRoute(
return;
}
// TODO: refactor this part to move into a common place
setCorsForThing(req, res, thing);
setCorsForThing(req, res, thing, this.getAllowedOrigins());
let corsPreflightWithCredentials = false;
const securityScheme = thing.securityDefinitions[Helpers.toStringArray(thing.security)[0]].scheme;

Expand Down Expand Up @@ -109,7 +109,7 @@ export default async function eventRoute(
} else {
// may have been OPTIONS that failed the credentials check
// as a result, we pass corsPreflightWithCredentials
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials, this.getAllowedOrigins());
}
// resource found and response sent
}
4 changes: 2 additions & 2 deletions packages/binding-http/src/routes/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default async function propertiesRoute(
}

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

Expand Down Expand Up @@ -86,6 +86,6 @@ export default async function propertiesRoute(
} else {
// may have been OPTIONS that failed the credentials check
// as a result, we pass corsPreflightWithCredentials
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials, this.getAllowedOrigins());
}
}
4 changes: 2 additions & 2 deletions packages/binding-http/src/routes/property-observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default async function propertyObserveRoute(
}

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

Expand Down Expand Up @@ -113,6 +113,6 @@ export default async function propertyObserveRoute(
res.writeHead(202);
res.end();
} else {
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials);
respondUnallowedMethod(req, res, "GET", corsPreflightWithCredentials, this.getAllowedOrigins());
}
}
6 changes: 3 additions & 3 deletions packages/binding-http/src/routes/property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default async function propertyRoute(
}

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

Expand Down Expand Up @@ -108,7 +108,7 @@ export default async function propertyRoute(
} else if (req.method === "PUT") {
const readOnly: boolean = property.readOnly ?? false;
if (readOnly) {
respondUnallowedMethod(req, res, "GET, PUT");
respondUnallowedMethod(req, res, "GET, PUT", false, this.getAllowedOrigins());
return;
}

Expand All @@ -128,6 +128,6 @@ export default async function propertyRoute(
} else {
// may have been OPTIONS that failed the credentials check
// as a result, we pass corsPreflightWithCredentials
respondUnallowedMethod(req, res, "GET, PUT", corsPreflightWithCredentials);
respondUnallowedMethod(req, res, "GET, PUT", corsPreflightWithCredentials, this.getAllowedOrigins());
} // Property exists?
}
2 changes: 1 addition & 1 deletion packages/binding-http/src/routes/thing-description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export default async function thingDescriptionRoute(
const payload = await content.toBuffer();

negotiateLanguage(td, thing, req);
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Origin", this.getAllowedOrigins());
res.setHeader("Content-Type", contentType);
res.writeHead(200);
debug(`Sending HTTP response for TD with Content-Type ${contentType}.`);
Expand Down
2 changes: 1 addition & 1 deletion packages/binding-http/src/routes/things.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function thingsRoute(
res: ServerResponse,
_params: unknown
): void {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Origin", this.getAllowedOrigins());
res.setHeader("Content-Type", ContentSerdes.DEFAULT);
res.writeHead(200);
const list = [];
Expand Down
Loading