@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 +343 -0
- package/__index__.ts +5 -0
- package/package.json +49 -0
- package/src/config.ts +70 -0
- package/src/http.ts +145 -0
- package/src/node.ts +30 -0
- package/src/tls.ts +51 -0
- package/src/types.ts +21 -0
- package/support/cli.web_http.ts +39 -0
- package/support/test/dispatcher.ts +43 -0
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
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
|
+
}
|