Skip to content

Commit

Permalink
feat(binding-http): add http server middleware (#1027)
Browse files Browse the repository at this point in the history
* feat(binding-http): add http server middleware

Add a middleware for the http server to handle raw http requests

* docs(binding-http): add http middleware example

* fix(binding-http): export http middleware

* docs: http middleware in binding-http and cli readme

* fix(binding-http): middleware as a function

* docs(binding-http): remove port, fix HTTPConfig
  • Loading branch information
ilbertt authored Jul 7, 2023
1 parent 2ed25ab commit d998040
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 12 deletions.
57 changes: 47 additions & 10 deletions packages/binding-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,15 @@ The protocol binding can be configured using his constructor or trough servient

```ts
{
port?: number; // TCP Port to listen on
address?: string; // IP address or hostname of local interface to bind to
proxy?: HttpProxyConfig; // proxy configuration
allowSelfSigned?: boolean; // Accept self signed certificates
serverKey?: string; // HTTPs server secret key file
serverCert?: string; // HTTPs server certificate file
security?: TD.SecurityScheme; // Security scheme of the server
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]
port?: number; // TCP Port to listen on
address?: string; // IP address or hostname of local interface to bind to
proxy?: HttpProxyConfig; // proxy configuration
allowSelfSigned?: boolean; // Accept self signed certificates
serverKey?: string; // HTTPs server secret key file
serverCert?: string; // HTTPs server certificate file
security?: TD.SecurityScheme; // Security scheme of the server
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]
middleware?: MiddlewareRequestHandler; // the MiddlewareRequestHandler function. See [Adding a middleware] section below.
}
```

Expand Down Expand Up @@ -303,10 +304,46 @@ The exposed thing on the internal server will product form URLs such as:
"href": "https://wot.w3.org/things/smart-coffee-machine/actions/makeDrink"
```

**baseUrt vs address**
**baseUri vs address**

> `baseUri` tells the producer to prefix URLs which may include hostnames, network interfaces, and URI prefixes which are not local to the machine exposing the Thing.
> `address` tells the HttpServer a specific ocal network interface to bind its TCP listener.

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

### 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).

This can be done by passing a middleware function to the HttpServer constructor.

```js
const { Servient } = require("@node-wot/core");
const { HttpServer } = require("@node-wot/binding-http");

const servient = new Servient();

const middleware = async (req, res, next) => {
// For example, reject requests in which the X-Custom-Header header is missing
// by replying with 400 Bad Request
if (!req.headers["x-custom-header"]) {
res.statusCode = 400;
res.end("Bad Request");
return;
}
// Pass all other requests to the WoT Servient
next();
};

const httpServer = new HttpServer({
middleware,
});

servient.addServer(httpServer);

servient.start().then(async (WoT) => {
// ...
});
```

## Feature matrix

Expand Down
62 changes: 62 additions & 0 deletions packages/binding-http/src/http-server-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/********************************************************************************
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
* Document License (2015-05-13) which is available at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
*
* SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
********************************************************************************/

/**
* HTTP Middleware for the HTTP Server
*/

import * as http from "http";

/**
* A middleware function for the HTTP server, which can be used to intercept requests before they are handled by the WoT Servient.
*
* Example:
* ```javascript
* import { Servient } from "@node-wot/core";
* import { HttpServer, MiddlewareRequestHandler } from "@node-wot/binding-http";
*
* const servient = new Servient();
*
* const middleware: MiddlewareRequestHandler = async (req, res, next) => {
* // For example, reject requests in which the X-Custom-Header header is missing
* // by replying with 400 Bad Request
* if (!req.headers["x-custom-header"]) {
* res.statusCode = 400;
* res.end("Bad Request");
* return;
* }
* // Pass all other requests to the WoT Servient
* next();
* };
* const httpServer = new HttpServer({
* middleware,
* });
*
* servient.addServer(httpServer);
*
* servient.start().then(async (WoT) => {
* // ...
* });
* ```
* @param req The HTTP request.
* @param res The HTTP response.
* @param next Call this function to pass the request to the WoT Servient.
*/
export type MiddlewareRequestHandler = (
req: http.IncomingMessage,
res: http.ServerResponse,
next: () => void
) => Promise<void>;
21 changes: 19 additions & 2 deletions packages/binding-http/src/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import slugify from "slugify";
import { ThingDescription } from "wot-typescript-definitions";
import * as acceptLanguageParser from "accept-language-parser";
import { ActionElement, EventElement, PropertyElement } from "wot-thing-description-types";
import { MiddlewareRequestHandler } from "./http-server-middleware";

const { debug, info, warn, error } = createLoggers("binding-http", "http-server");

Expand All @@ -65,6 +66,7 @@ export default class HttpServer implements ProtocolServer {
private readonly httpSecurityScheme: string = "NoSec"; // HTTP header compatible string
private readonly validOAuthClients: RegExp = /.*/g;
private readonly server: http.Server | https.Server = null;
private readonly middleware: MiddlewareRequestHandler = null;
private readonly things: Map<string, ExposedThing> = new Map<string, ExposedThing>();
private servient: Servient = null;
private oAuthValidator: Validator;
Expand Down Expand Up @@ -98,6 +100,9 @@ export default class HttpServer implements ProtocolServer {
if (config.urlRewrite !== undefined) {
this.urlRewrite = config.urlRewrite;
}
if (config.middleware !== undefined) {
this.middleware = config.middleware;
}

// TLS
if (config.serverKey && config.serverCert) {
Expand All @@ -106,12 +111,24 @@ export default class HttpServer implements ProtocolServer {
options.cert = fs.readFileSync(config.serverCert);
this.scheme = "https";
this.server = https.createServer(options, (req, res) => {
this.handleRequest(req, res);
if (this.middleware) {
this.middleware(req, res, () => {
this.handleRequest(req, res);
});
} else {
this.handleRequest(req, res);
}
});
} else {
this.scheme = "http";
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
if (this.middleware) {
this.middleware(req, res, () => {
this.handleRequest(req, res);
});
} else {
this.handleRequest(req, res);
}
});
}

Expand Down
3 changes: 3 additions & 0 deletions packages/binding-http/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@

import * as TD from "@node-wot/td-tools";
import { Method } from "./oauth-token-validation";
import { MiddlewareRequestHandler } from "./http-server-middleware";

export { default as HttpServer } from "./http-server";
export { default as HttpClient } from "./http-client";
export { default as HttpClientFactory } from "./http-client-factory";
export { default as HttpsClientFactory } from "./https-client-factory";
export { MiddlewareRequestHandler } from "./http-server-middleware";
export * from "./http-server";
export * from "./http-client";
export * from "./http-client-factory";
Expand All @@ -43,6 +45,7 @@ export interface HttpConfig {
serverKey?: string;
serverCert?: string;
security?: TD.SecurityScheme;
middleware?: MiddlewareRequestHandler;
}

export interface OAuth2ServerConfig extends TD.SecurityScheme {
Expand Down
51 changes: 51 additions & 0 deletions packages/binding-http/test/http-server-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { Content, createLoggers, ExposedThing, Helpers } from "@node-wot/core";
import { DataSchemaValue, InteractionInput, InteractionOptions } from "wot-typescript-definitions";
import chaiAsPromised from "chai-as-promised";
import { Readable } from "stream";
import { MiddlewareRequestHandler } from "../src/http-server-middleware";

const { debug, error } = createLoggers("binding-http", "http-server-test");

Expand All @@ -49,6 +50,56 @@ class HttpServerTest {
expect(httpServer.getPort()).to.eq(-1); // from getPort() when not listening
}

@test async "should use middleware if provided"() {
const middleware: MiddlewareRequestHandler = async (req, res, next) => {
if (req.url.endsWith("testMiddleware")) {
res.statusCode = 401;
res.end("Unauthorized");
} else {
next();
}
};

const httpServer = new HttpServer({
port,
middleware,
});

await httpServer.start(null);

const testThing = new ExposedThing(null, {
title: "Test",
properties: {
testMiddleware: {
forms: [],
},
testPassthrough: {
forms: [],
},
},
actions: {},
});

let test: DataSchemaValue;
testThing.setPropertyReadHandler("testMiddleware", () => Promise.resolve(test));
testThing.setPropertyReadHandler("testPassthrough", () => Promise.resolve(test));

await httpServer.expose(testThing);

const uri = `http://localhost:${httpServer.getPort()}/test/`;
let resp;

debug(`Testing ${uri}`);

resp = await fetch(uri + "properties/testMiddleware");
expect(resp.status).to.equal(401);

resp = await fetch(uri + "properties/testPassthrough");
expect(resp.status).to.equal(200);

return httpServer.stop();
}

@test async "should be able to destroy a thing"() {
const httpServer = new HttpServer({ port: 0 });

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ VAR2=Value2

Additionally, you can look at [the JSON Schema](https://github.com/eclipse-thingweb/node-wot/blob/master/packages/cli/src/wot-servient-schema.conf.json) to understand possible values for each field.

> In the current implementation, the **middleware** option (that you can use to handle raw HTTP requests _before_ they hit the Servient) is only available when using the `@node-wot/binding-http` package as a library. See [Adding a middleware](../binding-http/README.md#adding-a-middleware) for more information.
### Environment variables

If your scripts needs to access environment variables those must be supplied in a particular file. Node-wot cli uses [dotenv](https://github.com/motdotla/dotenv) library to load `.env` files located at the current working directory. For example, providing the following `.env` file will fill variables `PORT` and `ADDRESS` in scripts `process.env` field:
Expand Down

0 comments on commit d998040

Please sign in to comment.