@travetto/web-http 7.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,343 @@
1
+ <!-- This file was generated by @travetto/doc and should not be modified directly -->
2
+ <!-- Please modify https://github.com/travetto/travetto/tree/main/module/web-http/DOC.tsx and execute "npx trv doc" to rebuild -->
3
+ # Web HTTP Server Support
4
+
5
+ ## Web HTTP Server Support
6
+
7
+ **Install: @travetto/web-http**
8
+ ```bash
9
+ npm install @travetto/web-http
10
+
11
+ # or
12
+
13
+ yarn add @travetto/web-http
14
+ ```
15
+
16
+ This module provides basic for running [http](https://nodejs.org/api/http.html). [https](https://nodejs.org/api/https.html) and [http2](https://nodejs.org/api/http2.html) servers, along with support for tls key generation during development.
17
+
18
+ ## Running a Server
19
+ By default, the framework provides a default [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L85) for [WebHttpServer](https://github.com/travetto/travetto/tree/main/module/web-http/src/types.ts#L19) that will follow default behaviors, and spin up the server.
20
+
21
+ **Terminal: Standard application**
22
+ ```bash
23
+ $ trv web:http
24
+
25
+ Initialized {
26
+ manifest: {
27
+ main: {
28
+ name: '@travetto-doc/web-http',
29
+ folder: './doc-exec'
30
+ },
31
+ workspace: {
32
+ name: '@travetto-doc/web-http',
33
+ path: './doc-exec',
34
+ mono: false,
35
+ manager: 'npm',
36
+ type: 'commonjs',
37
+ defaultEnv: 'local'
38
+ }
39
+ },
40
+ runtime: {
41
+ env: 'local',
42
+ debug: false,
43
+ production: false,
44
+ dynamic: false,
45
+ resourcePaths: [
46
+ './doc-exec/resources'
47
+ ],
48
+ profiles: []
49
+ },
50
+ config: {
51
+ sources: [ { priority: 999, source: 'memory://override' } ],
52
+ active: {
53
+ AcceptConfig: { applies: false, types: [] },
54
+ CacheControlConfig: { applies: true },
55
+ CompressConfig: {
56
+ applies: true,
57
+ supportedEncodings: [ 'br', 'gzip', 'identity', 'deflate' ]
58
+ },
59
+ CookieConfig: { applies: true, httponly: true, sameSite: 'lax', path: '/' },
60
+ CorsConfig: { applies: true },
61
+ DecompressConfig: {
62
+ applies: true,
63
+ supportedEncodings: [ 'br', 'gzip', 'deflate', 'identity' ]
64
+ },
65
+ EtagConfig: { applies: true, minimumSize: '10kb' },
66
+ TrustProxyConfig: { applies: true, ips: [] },
67
+ WebBodyConfig: {
68
+ applies: true,
69
+ limit: '1mb',
70
+ parsingTypes: {
71
+ text: 'text',
72
+ 'application/json': 'json',
73
+ 'application/x-www-form-urlencoded': 'form'
74
+ }
75
+ },
76
+ WebConfig: { defaultMessage: true },
77
+ WebHttpConfig: {
78
+ httpVersion: '1.1',
79
+ port: 3000,
80
+ bindAddress: '0.0.0.0',
81
+ tls: false
82
+ },
83
+ WebLogConfig: { applies: true, showStackTrace: true }
84
+ }
85
+ }
86
+ }
87
+ Listening on port { port: 3000 }
88
+ ```
89
+
90
+ ### Configuration
91
+
92
+ **Code: Standard Web Http Config**
93
+ ```typescript
94
+ export class WebHttpConfig {
95
+
96
+ /**
97
+ * What version of HTTP to use
98
+ * Version 2 requires SSL for direct browser access
99
+ */
100
+ @EnvVar('WEB_HTTP_VERSION')
101
+ httpVersion: '1.1' | '2' = '1.1';
102
+
103
+ /**
104
+ * The port to run on
105
+ */
106
+ @EnvVar('WEB_HTTP_PORT')
107
+ port: number = 3000;
108
+
109
+ /**
110
+ * The bind address, defaults to 0.0.0.0
111
+ */
112
+ bindAddress: string = '';
113
+
114
+ /**
115
+ * Is TLS active
116
+ */
117
+ @EnvVar('WEB_HTTP_TLS')
118
+ tls?: boolean;
119
+
120
+ /**
121
+ * TLS Keys
122
+ */
123
+ @Secret()
124
+ tlsKeys?: WebSecureKeyPair;
125
+
126
+ @Ignore()
127
+ fetchUrl: string;
128
+
129
+ async postConstruct(): Promise<void> {
130
+ this.tls ??= (this.httpVersion === '2' || !!this.tlsKeys);
131
+ this.port = (this.port < 0 ? await NetUtil.getFreePort() : this.port);
132
+ this.bindAddress ||= await NetUtil.getLocalAddress();
133
+
134
+ if (!this.tls) {
135
+ // Clear out keys if ssl is not set
136
+ this.tlsKeys = undefined;
137
+ } else if (!this.tlsKeys) {
138
+ if (Runtime.production) {
139
+ throw new AppError('Default ssl keys are only valid for development use, please specify a config value at web.ssl.keys');
140
+ }
141
+ this.tlsKeys = await WebTlsUtil.generateKeyPair();
142
+ } else {
143
+ if (this.tlsKeys.key.length < 100) { // We have files or resources
144
+ this.tlsKeys.key = (await RuntimeResources.read(this.tlsKeys.key, true)).toString('utf8');
145
+ this.tlsKeys.cert = (await RuntimeResources.read(this.tlsKeys.cert, true)).toString('utf8');
146
+ }
147
+ }
148
+
149
+ this.fetchUrl = `${this.tls ? 'https' : 'http'}://${this.bindAddress}:${this.port}`;
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### Creating a Custom CLI Entry Point
155
+ To customize a Web server, you may need to construct an entry point using the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L85) decorator. This could look like:
156
+
157
+ **Code: Application entry point for Web Applications**
158
+ ```typescript
159
+ import { Env, toConcrete } from '@travetto/runtime';
160
+ import { CliCommand } from '@travetto/cli';
161
+ import { DependencyRegistryIndex } from '@travetto/di';
162
+ import { Registry } from '@travetto/registry';
163
+ import { WebHttpServer, WebHttpConfig } from '@travetto/web-http';
164
+
165
+ import './config-override.ts';
166
+
167
+ @CliCommand({ runTarget: true })
168
+ export class SampleApp {
169
+
170
+ preMain(): void {
171
+ Env.TRV_ENV.set('prod');
172
+ Env.NODE_ENV.set('production');
173
+ }
174
+
175
+ async main() {
176
+ console.log('CUSTOM STARTUP');
177
+ await Registry.init();
178
+ const ssl = await DependencyRegistryIndex.getInstance(WebHttpConfig);
179
+ ssl.tls = true;
180
+
181
+ // Configure server before running
182
+ const instance = await DependencyRegistryIndex.getInstance(toConcrete<WebHttpServer>());
183
+ const { complete } = await instance.serve();
184
+ return complete;
185
+ }
186
+ }
187
+ ```
188
+
189
+ And using the pattern established in the [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") module, you would run your program using `npx trv web:custom`.
190
+
191
+ **Terminal: Custom application**
192
+ ```bash
193
+ $ trv web:custom
194
+
195
+ CUSTOM STARTUP
196
+ Initialized {
197
+ manifest: {
198
+ main: {
199
+ name: '@travetto-doc/web-http',
200
+ folder: './doc-exec'
201
+ },
202
+ workspace: {
203
+ name: '@travetto-doc/web-http',
204
+ path: './doc-exec',
205
+ mono: false,
206
+ manager: 'npm',
207
+ type: 'commonjs',
208
+ defaultEnv: 'local'
209
+ }
210
+ },
211
+ runtime: {
212
+ env: 'prod',
213
+ debug: false,
214
+ production: true,
215
+ dynamic: false,
216
+ resourcePaths: [
217
+ './doc-exec/resources'
218
+ ],
219
+ profiles: []
220
+ },
221
+ config: {
222
+ sources: [
223
+ { priority: 10, source: 'direct' },
224
+ { priority: 999, source: 'memory://override' }
225
+ ],
226
+ active: {
227
+ AcceptConfig: { applies: false, types: [] },
228
+ CacheControlConfig: { applies: true },
229
+ CompressConfig: {
230
+ applies: true,
231
+ supportedEncodings: [ 'br', 'gzip', 'identity', 'deflate' ]
232
+ },
233
+ CookieConfig: { applies: true, httponly: true, sameSite: 'lax', path: '/' },
234
+ CorsConfig: { applies: true },
235
+ DecompressConfig: {
236
+ applies: true,
237
+ supportedEncodings: [ 'br', 'gzip', 'deflate', 'identity' ]
238
+ },
239
+ EtagConfig: { applies: true, minimumSize: '10kb' },
240
+ TrustProxyConfig: { applies: true, ips: [] },
241
+ WebBodyConfig: {
242
+ applies: true,
243
+ limit: '1mb',
244
+ parsingTypes: {
245
+ text: 'text',
246
+ 'application/json': 'json',
247
+ 'application/x-www-form-urlencoded': 'form'
248
+ }
249
+ },
250
+ WebConfig: { defaultMessage: true },
251
+ WebHttpConfig: {
252
+ httpVersion: '1.1',
253
+ port: 3000,
254
+ bindAddress: '0.0.0.0',
255
+ tls: true
256
+ },
257
+ WebLogConfig: { applies: true, showStackTrace: true }
258
+ }
259
+ }
260
+ }
261
+ Listening on port { port: 3000 }
262
+ ```
263
+
264
+ ## Node Web Http Server
265
+
266
+ **Code: Implementation**
267
+ ```typescript
268
+ export class NodeWebHttpServer implements WebHttpServer {
269
+
270
+ @Inject()
271
+ serverConfig: WebHttpConfig;
272
+
273
+ @Inject()
274
+ router: StandardWebRouter;
275
+
276
+ @Inject()
277
+ configService: ConfigurationService;
278
+
279
+ async serve(): Promise<WebServerHandle> {
280
+ const handle = await WebHttpUtil.startHttpServer({ ...this.serverConfig, dispatcher: this.router, });
281
+ console.log('Initialized', await this.configService.initBanner());
282
+ console.log('Listening', { port: this.serverConfig.port });
283
+ return handle;
284
+ }
285
+ }
286
+ ```
287
+
288
+ Current the [NodeWebHttpServer](https://github.com/travetto/travetto/tree/main/module/web-http/src/node.ts#L13) is the only provided [WebHttpServer](https://github.com/travetto/travetto/tree/main/module/web-http/src/types.ts#L19) implementation. It supports http/1.1, http/2, and tls, and is the same foundation as used by express, koa, and other popular frameworks.
289
+
290
+ ## Standard Utilities
291
+ The module also provides standard utilities for starting http servers programmatically:
292
+
293
+ **Code: Web Http Utilities**
294
+ ```typescript
295
+ export class WebHttpUtil {
296
+ /**
297
+ * Build a simple request handler
298
+ * @param dispatcher
299
+ */
300
+ static buildHandler(dispatcher: WebDispatcher): (req: HttpRequest, res: HttpResponse) => Promise<void>;
301
+ /**
302
+ * Start an http server
303
+ */
304
+ static async startHttpServer(config: WebHttpServerConfig): Promise<WebServerHandle<HttpServer>>;
305
+ /**
306
+ * Create a WebRequest given an incoming http request
307
+ */
308
+ static toWebRequest(req: HttpRequest): WebRequest;
309
+ /**
310
+ * Send WebResponse to outbound http response
311
+ */
312
+ static async respondToServerResponse(webRes: WebResponse, res: HttpResponse): Promise<void>;
313
+ }
314
+ ```
315
+
316
+ Specifically, looking at `buildHandler`,
317
+
318
+ **Code: Web Http Utilities**
319
+ ```typescript
320
+ static buildHandler(dispatcher: WebDispatcher): (req: HttpRequest, res: HttpResponse) => Promise<void> {
321
+ return async (req: HttpRequest, res: HttpResponse): Promise<void> => {
322
+ const request = this.toWebRequest(req);
323
+ const response = await dispatcher.dispatch({ request });
324
+ this.respondToServerResponse(response, res);
325
+ };
326
+ }
327
+ ```
328
+
329
+ we can see the structure for integrating the server behavior with the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module dispatcher:
330
+ * Converting the node primitive request to a [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11)
331
+ * Dispatching the request through the framework
332
+ * Receiving the [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) and sending that back over the primitive response.
333
+
334
+ ## TLS Support
335
+ Additionally the framework supports TLS out of the box, by allowing you to specify your public and private keys for the cert. In dev mode, the framework will also automatically generate a self-signed cert if:
336
+ * TLS support is configured
337
+ * [node-forge](https://www.npmjs.com/package/node-forge) is installed
338
+ * Not running in prod
339
+ * No keys provided
340
+
341
+ This is useful for local development where you implicitly trust the cert.
342
+
343
+ TLS support can be enabled by setting `web.http.tls: true` in your config. The key/cert can be specified as string directly in the config file/environment variables. The key/cert can also be specified as a path to be picked up by [RuntimeResources](https://github.com/travetto/travetto/tree/main/module/runtime/src/resources.ts#L8).
package/__index__.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './src/config.ts';
2
+ export * from './src/http.ts';
3
+ export * from './src/tls.ts';
4
+ export * from './src/types.ts';
5
+ export * from './src/node.ts';
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@travetto/web-http",
3
+ "version": "7.0.0-rc.0",
4
+ "description": "Web HTTP Server Support",
5
+ "keywords": [
6
+ "web",
7
+ "http",
8
+ "server",
9
+ "travetto",
10
+ "typescript"
11
+ ],
12
+ "homepage": "https://travetto.io",
13
+ "license": "MIT",
14
+ "author": {
15
+ "email": "travetto.framework@gmail.com",
16
+ "name": "Travetto Framework"
17
+ },
18
+ "files": [
19
+ "__index__.ts",
20
+ "src",
21
+ "support"
22
+ ],
23
+ "main": "__index__.ts",
24
+ "repository": {
25
+ "url": "git+https://github.com/travetto/travetto.git",
26
+ "directory": "module/web-http"
27
+ },
28
+ "dependencies": {
29
+ "@travetto/web": "^7.0.0-rc.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@travetto/cli": "^7.0.0-rc.0",
33
+ "@travetto/test": "^7.0.0-rc.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@travetto/test": {
37
+ "optional": true
38
+ },
39
+ "@travetto/cli": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "travetto": {
44
+ "displayName": "Web HTTP Server Support"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }
package/src/config.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { Config, EnvVar } from '@travetto/config';
2
+ import { Ignore, Secret } from '@travetto/schema';
3
+ import { AppError, Runtime, RuntimeResources } from '@travetto/runtime';
4
+ import { NetUtil } from '@travetto/web';
5
+
6
+ import { WebSecureKeyPair } from './types.ts';
7
+ import { WebTlsUtil } from './tls.ts';
8
+
9
+ /**
10
+ * Web HTTP configuration
11
+ */
12
+ @Config('web.http')
13
+ export class WebHttpConfig {
14
+
15
+ /**
16
+ * What version of HTTP to use
17
+ * Version 2 requires SSL for direct browser access
18
+ */
19
+ @EnvVar('WEB_HTTP_VERSION')
20
+ httpVersion: '1.1' | '2' = '1.1';
21
+
22
+ /**
23
+ * The port to run on
24
+ */
25
+ @EnvVar('WEB_HTTP_PORT')
26
+ port: number = 3000;
27
+
28
+ /**
29
+ * The bind address, defaults to 0.0.0.0
30
+ */
31
+ bindAddress: string = '';
32
+
33
+ /**
34
+ * Is TLS active
35
+ */
36
+ @EnvVar('WEB_HTTP_TLS')
37
+ tls?: boolean;
38
+
39
+ /**
40
+ * TLS Keys
41
+ */
42
+ @Secret()
43
+ tlsKeys?: WebSecureKeyPair;
44
+
45
+ @Ignore()
46
+ fetchUrl: string;
47
+
48
+ async postConstruct(): Promise<void> {
49
+ this.tls ??= (this.httpVersion === '2' || !!this.tlsKeys);
50
+ this.port = (this.port < 0 ? await NetUtil.getFreePort() : this.port);
51
+ this.bindAddress ||= await NetUtil.getLocalAddress();
52
+
53
+ if (!this.tls) {
54
+ // Clear out keys if ssl is not set
55
+ this.tlsKeys = undefined;
56
+ } else if (!this.tlsKeys) {
57
+ if (Runtime.production) {
58
+ throw new AppError('Default ssl keys are only valid for development use, please specify a config value at web.ssl.keys');
59
+ }
60
+ this.tlsKeys = await WebTlsUtil.generateKeyPair();
61
+ } else {
62
+ if (this.tlsKeys.key.length < 100) { // We have files or resources
63
+ this.tlsKeys.key = (await RuntimeResources.read(this.tlsKeys.key, true)).toString('utf8');
64
+ this.tlsKeys.cert = (await RuntimeResources.read(this.tlsKeys.cert, true)).toString('utf8');
65
+ }
66
+ }
67
+
68
+ this.fetchUrl = `${this.tls ? 'https' : 'http'}://${this.bindAddress}:${this.port}`;
69
+ }
70
+ }
package/src/http.ts ADDED
@@ -0,0 +1,145 @@
1
+ import net from 'node:net';
2
+ import http from 'node:http';
3
+ import http2 from 'node:http2';
4
+ import https from 'node:https';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { TLSSocket } from 'node:tls';
7
+
8
+ import { WebBodyUtil, WebCommonUtil, WebDispatcher, WebRequest, WebResponse } from '@travetto/web';
9
+ import { BinaryUtil, castTo, ShutdownManager } from '@travetto/runtime';
10
+
11
+ import { WebSecureKeyPair, WebServerHandle } from './types.ts';
12
+
13
+ type HttpServer = http.Server | http2.Http2Server;
14
+ type HttpResponse = http.ServerResponse | http2.Http2ServerResponse;
15
+ type HttpRequest = http.IncomingMessage | http2.Http2ServerRequest;
16
+ type HttpSocket = net.Socket | http2.Http2Stream;
17
+
18
+ type WebHttpServerConfig = {
19
+ httpVersion?: '1.1' | '2';
20
+ port: number;
21
+ bindAddress: string;
22
+ sslKeys?: WebSecureKeyPair;
23
+ dispatcher: WebDispatcher;
24
+ signal?: AbortSignal;
25
+ };
26
+
27
+ export class WebHttpUtil {
28
+
29
+ /**
30
+ * Build a simple request handler
31
+ * @param dispatcher
32
+ */
33
+ static buildHandler(dispatcher: WebDispatcher): (req: HttpRequest, res: HttpResponse) => Promise<void> {
34
+ return async (req: HttpRequest, res: HttpResponse): Promise<void> => {
35
+ const request = this.toWebRequest(req);
36
+ const response = await dispatcher.dispatch({ request });
37
+ this.respondToServerResponse(response, res);
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Start an http server
43
+ */
44
+ static async startHttpServer(config: WebHttpServerConfig): Promise<WebServerHandle<HttpServer>> {
45
+ const { reject, resolve, promise } = Promise.withResolvers<void>();
46
+
47
+ const handler = this.buildHandler(config.dispatcher);
48
+
49
+ let target: HttpServer;
50
+ if (config.httpVersion === '2') {
51
+ if (config.sslKeys) {
52
+ target = http2.createSecureServer(config.sslKeys, handler);
53
+ } else {
54
+ target = http2.createServer(handler);
55
+ }
56
+ } else {
57
+ if (config.sslKeys) {
58
+ target = https.createServer(config.sslKeys, handler);
59
+ } else {
60
+ target = http.createServer(handler);
61
+ }
62
+ }
63
+
64
+ const complete = new Promise<void>(r => target.on('close', r));
65
+
66
+ // Track connections for shutdown
67
+ const activeConnections = new Set<HttpSocket>();
68
+ target.on('connection', (socket: HttpSocket) => {
69
+ activeConnections.add(socket);
70
+ socket.on('close', () => activeConnections.delete(socket));
71
+ });
72
+
73
+ target.listen(config.port, config.bindAddress)
74
+ .on('error', reject)
75
+ .on('listening', resolve);
76
+
77
+ await promise;
78
+
79
+ target.off('error', reject);
80
+
81
+ async function stop(immediate?: boolean): Promise<void> {
82
+ if (!target.listening) {
83
+ return;
84
+ }
85
+ console.debug('Stopping http server');
86
+ target.close();
87
+ if (immediate) {
88
+ for (const connection of activeConnections) {
89
+ if (!connection.destroyed) {
90
+ connection.destroy();
91
+ }
92
+ }
93
+ }
94
+ return complete;
95
+ }
96
+
97
+ ShutdownManager.onGracefulShutdown(() => stop(false));
98
+ config.signal?.addEventListener('abort', () => stop(true));
99
+
100
+ return { target, complete, stop };
101
+ }
102
+
103
+ /**
104
+ * Create a WebRequest given an incoming http request
105
+ */
106
+ static toWebRequest(req: HttpRequest): WebRequest {
107
+ const secure = req.socket instanceof TLSSocket;
108
+ const [path, query] = (req.url ?? '/').split('?') ?? [];
109
+ return new WebRequest({
110
+ context: {
111
+ connection: {
112
+ ip: req.socket.remoteAddress!,
113
+ host: req.headers.host,
114
+ httpProtocol: secure ? 'https' : 'http',
115
+ port: req.socket.localPort
116
+ },
117
+ httpMethod: castTo(req.method?.toUpperCase()),
118
+ path,
119
+ httpQuery: Object.fromEntries(new URLSearchParams(query)),
120
+ },
121
+ headers: req.headers,
122
+ body: WebBodyUtil.markRaw(req)
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Send WebResponse to outbound http response
128
+ */
129
+ static async respondToServerResponse(webRes: WebResponse, res: HttpResponse): Promise<void> {
130
+ const binaryResponse = new WebResponse({ context: webRes.context, ...WebBodyUtil.toBinaryMessage(webRes) });
131
+ binaryResponse.headers.forEach((v, k) => res.setHeader(k, v));
132
+ res.statusCode = WebCommonUtil.getStatusCode(binaryResponse);
133
+ const body = binaryResponse.body;
134
+
135
+ if (BinaryUtil.isReadable(body)) {
136
+ await pipeline(body, res);
137
+ } else {
138
+ if (body) {
139
+ // Weird type union that http2 uses
140
+ 'stream' in res ? res.write(body) : res.write(body);
141
+ }
142
+ res.end();
143
+ }
144
+ }
145
+ }
package/src/node.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { Inject, Injectable } from '@travetto/di';
2
+ import { StandardWebRouter } from '@travetto/web';
3
+ import { ConfigurationService } from '@travetto/config';
4
+
5
+ import { WebHttpConfig } from './config';
6
+ import { WebHttpUtil } from './http';
7
+ import { WebHttpServer, WebServerHandle } from './types';
8
+
9
+ /**
10
+ * A node http server
11
+ */
12
+ @Injectable()
13
+ export class NodeWebHttpServer implements WebHttpServer {
14
+
15
+ @Inject()
16
+ serverConfig: WebHttpConfig;
17
+
18
+ @Inject()
19
+ router: StandardWebRouter;
20
+
21
+ @Inject()
22
+ configService: ConfigurationService;
23
+
24
+ async serve(): Promise<WebServerHandle> {
25
+ const handle = await WebHttpUtil.startHttpServer({ ...this.serverConfig, dispatcher: this.router, });
26
+ console.log('Initialized', await this.configService.initBanner());
27
+ console.log('Listening', { port: this.serverConfig.port });
28
+ return handle;
29
+ }
30
+ }
package/src/tls.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { PackageUtil } from '@travetto/manifest';
2
+ import { Runtime } from '@travetto/runtime';
3
+
4
+ import { WebSecureKeyPair } from './types.ts';
5
+
6
+ /**
7
+ * Utils for generating key pairs
8
+ */
9
+ export class WebTlsUtil {
10
+
11
+ /**
12
+ * Generate TLS key pair on demand
13
+ * @param subj The subject for the app
14
+ */
15
+ static async generateKeyPair(subj = { C: 'US', ST: 'CA', O: 'TRAVETTO', OU: 'WEB', CN: 'DEV' }): Promise<WebSecureKeyPair> {
16
+ let forge;
17
+
18
+ try {
19
+ forge = (await import('node-forge')).default;
20
+ } catch {
21
+ const install = PackageUtil.getInstallCommand(Runtime, 'node-forge');
22
+ throw new Error(`In order to generate TLS keys, you must install node-forge, "${install}"`);
23
+ }
24
+
25
+ const pki = forge.pki;
26
+
27
+ const keys = pki.rsa.generateKeyPair(2048);
28
+ const cert = pki.createCertificate();
29
+
30
+ // fill the required fields
31
+ cert.publicKey = keys.publicKey;
32
+ cert.serialNumber = '01';
33
+ cert.validity.notBefore = new Date();
34
+ cert.validity.notAfter = new Date();
35
+ cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
36
+
37
+ const attrs = Object.entries(subj).map(([shortName, value]) => ({ shortName, value }));
38
+
39
+ // here we set subject and issuer as the same one
40
+ cert.setSubject(attrs);
41
+ cert.setIssuer(attrs);
42
+
43
+ // the actual certificate signing
44
+ cert.sign(keys.privateKey);
45
+
46
+ return {
47
+ cert: pki.certificateToPem(cert),
48
+ key: pki.privateKeyToPem(keys.privateKey)
49
+ };
50
+ }
51
+ }
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { Any } from '@travetto/runtime';
2
+
3
+ export type WebSecureKeyPair = { cert: string, key: string };
4
+
5
+ /**
6
+ * Handle for a web server
7
+ */
8
+ export type WebServerHandle<T = Any> = {
9
+ target: T;
10
+ complete: Promise<void>;
11
+ stop: (immediate?: boolean) => Promise<void>;
12
+ };
13
+
14
+ /**
15
+ * Defines the shape of the web server
16
+ *
17
+ * @concrete
18
+ */
19
+ export interface WebHttpServer {
20
+ serve(): Promise<WebServerHandle>;
21
+ }
@@ -0,0 +1,39 @@
1
+ import { Runtime, toConcrete, Util } from '@travetto/runtime';
2
+ import { DependencyRegistryIndex } from '@travetto/di';
3
+ import { CliCommand, CliCommandShape } from '@travetto/cli';
4
+ import { NetUtil } from '@travetto/web';
5
+ import { Registry } from '@travetto/registry';
6
+
7
+ import type { WebHttpServer } from '../src/types.ts';
8
+
9
+ /**
10
+ * Run a web server
11
+ */
12
+ @CliCommand({ runTarget: true, with: { debugIpc: true, canRestart: true, module: true, env: true } })
13
+ export class WebHttpCommand implements CliCommandShape {
14
+
15
+ /** Port to run on */
16
+ port?: number;
17
+
18
+ /** Kill conflicting port owner */
19
+ killConflict?: boolean;
20
+
21
+ preMain(): void {
22
+ if (this.port) {
23
+ process.env.WEB_HTTP_PORT = `${this.port}`;
24
+ }
25
+ }
26
+
27
+ async main(): Promise<void> {
28
+ await Registry.init();
29
+ const instance = await DependencyRegistryIndex.getInstance(toConcrete<WebHttpServer>());
30
+
31
+ const handle = await Util.acquireWithRetry(
32
+ () => instance.serve(),
33
+ NetUtil.freePortOnConflict,
34
+ this.killConflict && !Runtime.production ? 5 : 1
35
+ );
36
+
37
+ return handle.complete;
38
+ }
39
+ }
@@ -0,0 +1,43 @@
1
+ import { Readable } from 'node:stream';
2
+ import { buffer } from 'node:stream/consumers';
3
+
4
+ import { Inject, Injectable } from '@travetto/di';
5
+ import { WebFilterContext, WebResponse, WebDispatcher, WebBodyUtil } from '@travetto/web';
6
+ import { castTo } from '@travetto/runtime';
7
+
8
+
9
+ import { WebTestDispatchUtil } from '@travetto/web/support/test/dispatch-util.ts';
10
+
11
+ import { WebHttpConfig } from '../../src/config.ts';
12
+
13
+ const toBuffer = (src: Buffer | Readable) => Buffer.isBuffer(src) ? src : buffer(src);
14
+
15
+ /**
16
+ * Support for invoking http requests against the server
17
+ */
18
+ @Injectable()
19
+ export class FetchWebDispatcher implements WebDispatcher {
20
+
21
+ @Inject()
22
+ config: WebHttpConfig;
23
+
24
+ async dispatch({ request }: WebFilterContext): Promise<WebResponse> {
25
+ const baseRequest = await WebTestDispatchUtil.applyRequestBody(request);
26
+ const finalPath = WebTestDispatchUtil.buildPath(baseRequest);
27
+ const body: RequestInit['body'] = WebBodyUtil.isRaw(request.body) ? await toBuffer(request.body) : castTo(request.body);
28
+ const { context: { httpMethod: method }, headers } = request;
29
+
30
+ const response = await fetch(
31
+ `${this.config.fetchUrl}${finalPath}`,
32
+ { method, headers, body }
33
+ );
34
+
35
+ return await WebTestDispatchUtil.finalizeResponseBody(
36
+ new WebResponse({
37
+ body: Buffer.from(await response.arrayBuffer()),
38
+ context: { httpStatusCode: response.status },
39
+ headers: response.headers
40
+ })
41
+ );
42
+ }
43
+ }