@superhero/http-server 4.0.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/LICENCE +21 -0
- package/README.md +451 -0
- package/config.json +18 -0
- package/index.js +424 -0
- package/index.test.js +464 -0
- package/middleware/upstream/header/accept.js +52 -0
- package/middleware/upstream/header/content-type/application/json.js +29 -0
- package/middleware/upstream/header/content-type.js +45 -0
- package/middleware/upstream/method.js +38 -0
- package/package.json +42 -0
- package/view.js +285 -0
package/index.test.js
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import util from 'node:util'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import https from 'node:https'
|
|
6
|
+
import tls from 'node:tls'
|
|
7
|
+
import Request from '@superhero/http-request'
|
|
8
|
+
import Router from '@superhero/router'
|
|
9
|
+
import Locator from '@superhero/locator'
|
|
10
|
+
import HttpServer from '@superhero/http-server'
|
|
11
|
+
import { execSync } from 'node:child_process'
|
|
12
|
+
import { afterEach, beforeEach, suite, test } from 'node:test'
|
|
13
|
+
|
|
14
|
+
util.inspect.defaultOptions.depth = 10
|
|
15
|
+
|
|
16
|
+
suite('@superhero/http-server', () =>
|
|
17
|
+
{
|
|
18
|
+
let locator, server
|
|
19
|
+
|
|
20
|
+
beforeEach(async () =>
|
|
21
|
+
{
|
|
22
|
+
if(beforeEach.skip)
|
|
23
|
+
{
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
locator = new Locator()
|
|
28
|
+
|
|
29
|
+
await locator.eagerload(
|
|
30
|
+
{
|
|
31
|
+
'@superhero/http-server': path.resolve('./index.js')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
server = locator.locate('@superhero/http-server')
|
|
35
|
+
server.log.info = () => null
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
afterEach (() =>
|
|
39
|
+
{
|
|
40
|
+
if(afterEach.skip)
|
|
41
|
+
{
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
locator.clear()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
suite('Lifecycle', async () =>
|
|
48
|
+
{
|
|
49
|
+
test('Can instantiate HttpServer', () =>
|
|
50
|
+
{
|
|
51
|
+
const router = new Router(locator)
|
|
52
|
+
assert.doesNotThrow(() => new HttpServer(router))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('Can bootstrap server with non-secure settings', async () =>
|
|
56
|
+
{
|
|
57
|
+
await server.bootstrap()
|
|
58
|
+
|
|
59
|
+
assert.ok(server.http1Server)
|
|
60
|
+
assert.ok(server.http1Server.constructor.name === 'Server')
|
|
61
|
+
|
|
62
|
+
assert.ok(server.http2Server)
|
|
63
|
+
assert.ok(server.http2Server.constructor.name === 'Http2Server')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('Listens and closes the server as expected', async () =>
|
|
67
|
+
{
|
|
68
|
+
await server.bootstrap()
|
|
69
|
+
await assert.doesNotReject(server.listen())
|
|
70
|
+
await assert.doesNotReject(server.close())
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('Rejects if server is not available to listen error', async () =>
|
|
74
|
+
{
|
|
75
|
+
await assert.rejects(
|
|
76
|
+
server.listen(),
|
|
77
|
+
(error) => error.code === 'E_HTTP_SERVER_NOT_AVAILIBLE')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('Rejects if server is not available to close error', async () =>
|
|
81
|
+
{
|
|
82
|
+
await assert.rejects(
|
|
83
|
+
server.close(),
|
|
84
|
+
(error) => error.code === 'E_HTTP_SERVER_NOT_AVAILIBLE')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
suite('Routing and Requests', async () =>
|
|
89
|
+
{
|
|
90
|
+
let request
|
|
91
|
+
|
|
92
|
+
beforeEach(async () =>
|
|
93
|
+
{
|
|
94
|
+
await server.bootstrap()
|
|
95
|
+
await server.listen()
|
|
96
|
+
const { port } = server.gateway.address()
|
|
97
|
+
request = new Request(
|
|
98
|
+
{
|
|
99
|
+
base: `http://localhost:${port}`,
|
|
100
|
+
timeout: 1e3,
|
|
101
|
+
doNotThrowOnErrorStatus: true
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
afterEach(async () =>
|
|
106
|
+
{
|
|
107
|
+
await server.close()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
suite('HTTP/1', () =>
|
|
111
|
+
{
|
|
112
|
+
beforeEach(() => request.config.headers = { 'connection': 'close' })
|
|
113
|
+
sameTestsForBothProtocols()
|
|
114
|
+
|
|
115
|
+
test('Support connection keep-alive header', async () =>
|
|
116
|
+
{
|
|
117
|
+
server.router.setRoutes(
|
|
118
|
+
{ 'foo':
|
|
119
|
+
{ criteria : '/test/foo',
|
|
120
|
+
dispatcher : 'foo-dispatcher'
|
|
121
|
+
}})
|
|
122
|
+
|
|
123
|
+
locator.set('foo-dispatcher', { dispatch: () => null })
|
|
124
|
+
|
|
125
|
+
// Set the keep-alive timeout to 10 seconds on the server.
|
|
126
|
+
const keepAlive = 10
|
|
127
|
+
server.http1Server.keepAliveTimeout = keepAlive * 1e3
|
|
128
|
+
|
|
129
|
+
// Make a request with the connection header set to keep-alive.
|
|
130
|
+
const response1 = await request.get({ url: `/test/foo`, headers: { 'connection': 'keep-alive' }})
|
|
131
|
+
assert.equal(response1.status, 200, 'Should have received a 200 status')
|
|
132
|
+
assert.equal(response1.headers['connection'], 'keep-alive', 'Should echo the connection header')
|
|
133
|
+
assert.equal(response1.headers['keep-alive'], 'timeout=' + keepAlive, 'Should have a keep-alive header')
|
|
134
|
+
|
|
135
|
+
// Make a request with the connection header set to close.
|
|
136
|
+
const response2 = await request.get({ url: `/test/foo`, headers: { 'connection': 'close' }})
|
|
137
|
+
assert.equal(response2.status, 200, 'Should have received a 200 status')
|
|
138
|
+
assert.equal(response2.headers['connection'], 'close', 'Should echo the connection header')
|
|
139
|
+
assert.equal(response2.headers['keep-alive'], undefined, 'Should not have the keep-alive header')
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
suite('HTTP/2', () =>
|
|
144
|
+
{
|
|
145
|
+
beforeEach(() => request.connect())
|
|
146
|
+
afterEach(() => request.close())
|
|
147
|
+
sameTestsForBothProtocols()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
function sameTestsForBothProtocols()
|
|
151
|
+
{
|
|
152
|
+
test('Can dispatch a request aligned to the route map', async () =>
|
|
153
|
+
{
|
|
154
|
+
server.router.setRoutes(
|
|
155
|
+
{ 'foo':
|
|
156
|
+
{ criteria : '/test/foo',
|
|
157
|
+
dispatcher : 'foo-dispatcher'
|
|
158
|
+
},
|
|
159
|
+
'bar':
|
|
160
|
+
{ criteria : '/test/bar',
|
|
161
|
+
dispatcher : 'bar-dispatcher' }})
|
|
162
|
+
|
|
163
|
+
let dispatched
|
|
164
|
+
locator.set('foo-dispatcher', { dispatch: () => dispatched = 'foo' })
|
|
165
|
+
locator.set('bar-dispatcher', { dispatch: () => dispatched = 'bar' })
|
|
166
|
+
|
|
167
|
+
const fooResponse = await request.get(`/test/foo`)
|
|
168
|
+
|
|
169
|
+
assert.equal(fooResponse.status, 200, 'Should have received a 200 status')
|
|
170
|
+
assert.equal(dispatched, 'foo', 'Should have dispatched the foo route')
|
|
171
|
+
|
|
172
|
+
const barResponse = await request.get(`/test/bar`)
|
|
173
|
+
assert.equal(barResponse.status, 200, 'Should have received a 200 status for the bar request')
|
|
174
|
+
assert.equal(dispatched, 'bar', 'Should have dispatched the bar route')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('Can alter the output body', async () =>
|
|
178
|
+
{
|
|
179
|
+
server.router.setRoutes(
|
|
180
|
+
{ 'foo':
|
|
181
|
+
{ criteria : '/test/foo',
|
|
182
|
+
dispatcher : 'foo-dispatcher' }})
|
|
183
|
+
|
|
184
|
+
locator.set('foo-dispatcher',
|
|
185
|
+
{ dispatch: (_, session) => session.view.body.foo = 'bar' })
|
|
186
|
+
|
|
187
|
+
const response = await request.get(`/test/foo`)
|
|
188
|
+
assert.equal(response.status, 200, 'Should have received a 200 status')
|
|
189
|
+
assert.equal(response.body.foo, 'bar', 'Response body should have been altered')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test('Can stream HTML5 standard Server-Sent Events (SSE)', async () =>
|
|
193
|
+
{
|
|
194
|
+
server.router.setRoutes(
|
|
195
|
+
{ 'sse':
|
|
196
|
+
{ criteria : '/test/sse',
|
|
197
|
+
dispatcher : 'sse-dispatcher' }})
|
|
198
|
+
|
|
199
|
+
locator.set('sse-dispatcher',
|
|
200
|
+
{ dispatch: (_, session) =>
|
|
201
|
+
{
|
|
202
|
+
session.view.stream.write({ foo: 'bar' })
|
|
203
|
+
session.view.stream.write({ bar: 'baz' })
|
|
204
|
+
session.view.stream.write({ baz: 'qux' })
|
|
205
|
+
session.view.stream.end()
|
|
206
|
+
}})
|
|
207
|
+
|
|
208
|
+
const response = await request.get(`/test/sse`)
|
|
209
|
+
|
|
210
|
+
assert.equal(response.status, 200, 'Should have received a 200 status')
|
|
211
|
+
assert.equal(response.body.length, 3, 'Response body should have three records')
|
|
212
|
+
assert.equal(response.body[0]?.data?.foo, 'bar', 'Response body should have the first record')
|
|
213
|
+
assert.equal(response.body[1]?.data?.bar, 'baz', 'Response body should have the second record')
|
|
214
|
+
assert.equal(response.body[2]?.data?.baz, 'qux', 'Response body should have the third record')
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('Can alter the output headers', async () =>
|
|
218
|
+
{
|
|
219
|
+
server.router.setRoutes(
|
|
220
|
+
{ 'foo':
|
|
221
|
+
{ criteria : '/test/foo',
|
|
222
|
+
dispatcher : 'foo-dispatcher' }})
|
|
223
|
+
|
|
224
|
+
locator.set('foo-dispatcher',
|
|
225
|
+
{ dispatch: (_, session) => session.view.headers.foo = 'bar' })
|
|
226
|
+
|
|
227
|
+
const response = await request.get(`/test/foo`)
|
|
228
|
+
assert.equal(response.status, 200, 'Should have received a 200 status')
|
|
229
|
+
assert.equal(response.headers.foo, 'bar', 'Response header should have been altered')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('Can alter the output status', async () =>
|
|
233
|
+
{
|
|
234
|
+
server.router.setRoutes(
|
|
235
|
+
{ 'foo':
|
|
236
|
+
{ criteria : '/test/foo',
|
|
237
|
+
dispatcher : 'foo-dispatcher' }})
|
|
238
|
+
|
|
239
|
+
locator.set('foo-dispatcher',
|
|
240
|
+
{ dispatch: (_, session) => session.view.status = 204 })
|
|
241
|
+
|
|
242
|
+
const response = await request.get(`/test/foo`)
|
|
243
|
+
assert.equal(response.status, 204, 'Should have received a 204 status')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('Can abort the dispatcher', async () =>
|
|
247
|
+
{
|
|
248
|
+
server.router.setRoutes(
|
|
249
|
+
{ 'foo':
|
|
250
|
+
{ criteria : '/test/foo',
|
|
251
|
+
dispatcher : 'foo-dispatcher' }})
|
|
252
|
+
|
|
253
|
+
locator.set('foo-dispatcher',
|
|
254
|
+
{ dispatch: (_, session) => session.abortion.abort(new Error('Aborted')) })
|
|
255
|
+
|
|
256
|
+
const response = await request.get(`/test/foo`)
|
|
257
|
+
|
|
258
|
+
assert.equal(response.status, 500, 'Should have failed with a 500 status')
|
|
259
|
+
assert.equal(response.body.error, 'Aborted', 'Response body should have the abortion message')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('Can describe an abortion in detail', async () =>
|
|
263
|
+
{
|
|
264
|
+
server.router.setRoutes(
|
|
265
|
+
{ 'foo':
|
|
266
|
+
{ criteria : '/test/foo',
|
|
267
|
+
dispatcher : 'foo-dispatcher' }})
|
|
268
|
+
|
|
269
|
+
locator.set('foo-dispatcher',
|
|
270
|
+
{
|
|
271
|
+
dispatch: (_, session) =>
|
|
272
|
+
{
|
|
273
|
+
const error = new Error('Aborted')
|
|
274
|
+
error.code = 'E_TEST_ABORT'
|
|
275
|
+
error.cause = new Error('Abortion test')
|
|
276
|
+
error.cause.code = 'E_TEST_ABORT_CAUSE'
|
|
277
|
+
error.cause.cause = 'Deeper detailed test'
|
|
278
|
+
session.abortion.abort(error)
|
|
279
|
+
}
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const response = await request.get(`/test/foo`)
|
|
283
|
+
|
|
284
|
+
assert.equal(response.status, 500, 'Should have failed with a 500 status')
|
|
285
|
+
assert.equal(response.body.error, 'Aborted', 'Response body should have the abortion message')
|
|
286
|
+
assert.equal(response.body.code, 'E_TEST_ABORT', 'Response body should have the abortion code')
|
|
287
|
+
assert.equal(response.body.details?.[0], 'E_TEST_ABORT_CAUSE - Abortion test', 'Response body should have the abortion cause')
|
|
288
|
+
assert.equal(response.body.details?.[1], 'Deeper detailed test', 'Response body should have the deeper detailed error message')
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
test('Can manage thrown errors in the dispatcher', async () =>
|
|
292
|
+
{
|
|
293
|
+
server.router.setRoutes(
|
|
294
|
+
{ 'foo':
|
|
295
|
+
{ criteria : '/test/foo',
|
|
296
|
+
dispatcher : 'foo-dispatcher' }})
|
|
297
|
+
|
|
298
|
+
locator.set('foo-dispatcher',
|
|
299
|
+
{
|
|
300
|
+
dispatch: () =>
|
|
301
|
+
{
|
|
302
|
+
const error = new Error('Failed dispatcher test')
|
|
303
|
+
error.code = 'E_TEST_FAILED_DISPATCHER'
|
|
304
|
+
throw error
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
let errorLoggerCalled = false
|
|
309
|
+
|
|
310
|
+
server.log.error = (error) =>
|
|
311
|
+
{
|
|
312
|
+
errorLoggerCalled = true
|
|
313
|
+
assert.equal(error.code, 'E_ROUTER_DISPATCH_FAILED', 'Should throw router error')
|
|
314
|
+
assert.equal(error.cause.code, 'E_TEST_FAILED_DISPATCHER', 'The error should have the dispatcher error as cause')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const response = await request.get(`/test/foo`)
|
|
318
|
+
|
|
319
|
+
assert.equal(response.status, 500, 'Should have failed with a 500 status')
|
|
320
|
+
assert.equal(response.body.error, 'Failed dispatcher test', 'Response body should have the error message')
|
|
321
|
+
assert.equal(response.body.code, 'E_TEST_FAILED_DISPATCHER', 'Response body should have the error code')
|
|
322
|
+
assert.equal(errorLoggerCalled, true, 'The error logger should have been called')
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
test('Can not mistakenly access the wrong view property', async () =>
|
|
326
|
+
{
|
|
327
|
+
server.router.setRoutes(
|
|
328
|
+
{ 'foo':
|
|
329
|
+
{ criteria : '/test/foo',
|
|
330
|
+
dispatcher : 'foo-dispatcher' }})
|
|
331
|
+
|
|
332
|
+
locator.set('foo-dispatcher',
|
|
333
|
+
{ dispatch: (_, session) => session.view.invalidAttribute })
|
|
334
|
+
|
|
335
|
+
let errorLoggerCalled = false
|
|
336
|
+
|
|
337
|
+
server.log.error = (error) =>
|
|
338
|
+
{
|
|
339
|
+
errorLoggerCalled = true
|
|
340
|
+
assert.equal(error.code, 'E_ROUTER_DISPATCH_FAILED')
|
|
341
|
+
assert.equal(error.cause.code, 'E_HTTP_SERVER_VIEW_MODEL_PROPERTY_NOT_READABLE')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const response = await request.get(`/test/foo`)
|
|
345
|
+
|
|
346
|
+
assert.equal(response.status, 500, 'Should have failed with a 500 status')
|
|
347
|
+
assert.equal(response.body.code, 'E_HTTP_SERVER_VIEW_MODEL_PROPERTY_NOT_READABLE')
|
|
348
|
+
assert.equal(errorLoggerCalled, true)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('Can not mistakenly assign a value to the wrong view property', async () =>
|
|
352
|
+
{
|
|
353
|
+
server.router.setRoutes(
|
|
354
|
+
{ 'foo':
|
|
355
|
+
{ criteria : '/test/foo',
|
|
356
|
+
dispatcher : 'foo-dispatcher' }})
|
|
357
|
+
|
|
358
|
+
locator.set('foo-dispatcher',
|
|
359
|
+
{ dispatch: (_, session) => session.view.invalidAttribute = 'This should not be possible' })
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
let errorLoggerCalled = false
|
|
363
|
+
|
|
364
|
+
server.log.error = (error) =>
|
|
365
|
+
{
|
|
366
|
+
errorLoggerCalled = true
|
|
367
|
+
assert.equal(error.code, 'E_ROUTER_DISPATCH_FAILED')
|
|
368
|
+
assert.equal(error.cause.code, 'E_HTTP_SERVER_VIEW_MODEL_PROPERTY_NOT_WRITABLE')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const response = await request.get(`/test/foo`)
|
|
372
|
+
|
|
373
|
+
assert.equal(response.status, 500, 'Should have failed with a 500 status')
|
|
374
|
+
assert.equal(response.body.code, 'E_HTTP_SERVER_VIEW_MODEL_PROPERTY_NOT_WRITABLE')
|
|
375
|
+
assert.equal(errorLoggerCalled, true)
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
suite('HTTPS server with self-signed certificate', async () =>
|
|
381
|
+
{
|
|
382
|
+
const
|
|
383
|
+
tlsVersions = ['TLSv1.2', 'TLSv1.3'],
|
|
384
|
+
algorithms =
|
|
385
|
+
{
|
|
386
|
+
'RSA:2048' : `openssl req -newkey rsa:2048 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
|
|
387
|
+
'RSA:4096' : `openssl req -newkey rsa:4096 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
|
|
388
|
+
'ECDSA:P-256' : `openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
|
|
389
|
+
'ECDSA:P-384' : `openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-384 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
|
|
390
|
+
'ECDSA:P-521' : `openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-521 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
|
|
391
|
+
'EdDSA:Ed25519' : `openssl req -newkey ed25519 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
|
|
392
|
+
'EdDSA:Ed448' : `openssl req -newkey ed448 -nodes -keyout test/private.key -x509 -days 365 -out test/server.cert -subj "/CN=localhost"`,
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for(const tlsVersion of tlsVersions)
|
|
396
|
+
{
|
|
397
|
+
suite(tlsVersion, () =>
|
|
398
|
+
{
|
|
399
|
+
for(const algorithm in algorithms)
|
|
400
|
+
{
|
|
401
|
+
test(algorithm, async (sub) =>
|
|
402
|
+
{
|
|
403
|
+
fs.mkdirSync('test', { recursive: true })
|
|
404
|
+
execSync(algorithms[algorithm], { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
405
|
+
|
|
406
|
+
const
|
|
407
|
+
cert = fs.readFileSync('test/server.cert').toString(),
|
|
408
|
+
key = fs.readFileSync('test/private.key').toString()
|
|
409
|
+
|
|
410
|
+
fs.rmSync('test', { recursive: true, force: true })
|
|
411
|
+
|
|
412
|
+
await assert.doesNotReject(server.bootstrap(
|
|
413
|
+
{ server: { cert, key, minVersion: tlsVersion, maxVersion: tlsVersion },
|
|
414
|
+
router:
|
|
415
|
+
{ routes:
|
|
416
|
+
{ 'test':
|
|
417
|
+
{ criteria : '/test',
|
|
418
|
+
dispatcher : 'test-dispatcher' }}}}))
|
|
419
|
+
|
|
420
|
+
locator.set('test-dispatcher',
|
|
421
|
+
{ dispatch: (_, session) => session.view.body.dispatched = true })
|
|
422
|
+
|
|
423
|
+
assert.ok(server.gateway instanceof tls.Server)
|
|
424
|
+
assert.ok(server.http1Server instanceof https.Server)
|
|
425
|
+
assert.ok(server.http2Server?.constructor.name === 'Http2SecureServer')
|
|
426
|
+
|
|
427
|
+
await assert.doesNotReject(server.listen())
|
|
428
|
+
|
|
429
|
+
const { port } = server.gateway.address()
|
|
430
|
+
const request = new Request({ base: `https://localhost:${port}`, timeout: 5e3, rejectUnauthorized: false })
|
|
431
|
+
|
|
432
|
+
beforeEach.skip = true
|
|
433
|
+
afterEach.skip = true
|
|
434
|
+
|
|
435
|
+
await sub.test('HTTP1', async () =>
|
|
436
|
+
{
|
|
437
|
+
const http1Response = await request.get({ url:'/test', headers: { 'connection': 'close' }})
|
|
438
|
+
assert.equal(http1Response.status, 200, 'Should have received a 200 status')
|
|
439
|
+
assert.equal(http1Response.body.dispatched, true, 'Should have dispatched the test route')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
await sub.test('HTTP2', async () =>
|
|
443
|
+
{
|
|
444
|
+
await request.connect()
|
|
445
|
+
const http2Response1 = await request.get('/test')
|
|
446
|
+
const http2Response2 = await request.get('/test')
|
|
447
|
+
await request.close()
|
|
448
|
+
|
|
449
|
+
assert.equal(http2Response1.status, 200, 'HTTP2 request 1 should have received a 200 status')
|
|
450
|
+
assert.equal(http2Response2.status, 200, 'HTTP2 request 2 should have received a 200 status')
|
|
451
|
+
assert.equal(http2Response1.body.dispatched, true, 'HTTP2 request 1 should have dispatched the test route')
|
|
452
|
+
assert.equal(http2Response2.body.dispatched, true, 'HTTP2 request 2 should have dispatched the test route')
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
delete beforeEach.skip
|
|
456
|
+
delete afterEach.skip
|
|
457
|
+
|
|
458
|
+
await assert.doesNotReject(server.close())
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
})
|
|
464
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @memberof @superhero/http-server:middleware/upstream/header
|
|
3
|
+
*/
|
|
4
|
+
export default new class AcceptHeaderUpstreamMiddleware
|
|
5
|
+
{
|
|
6
|
+
#listFormat = new Intl.ListFormat('en', { style:'long', type:'disjunction' })
|
|
7
|
+
#normalize = (route) => route.replace('accept-', '').trim()
|
|
8
|
+
|
|
9
|
+
dispatch(request, session)
|
|
10
|
+
{
|
|
11
|
+
const
|
|
12
|
+
splitHeader = request.headers['accept']?.toLowerCase().split(',') || [],
|
|
13
|
+
accepts = splitHeader.map(this.#normalize),
|
|
14
|
+
routes = Object.keys(session.route).filter((key) => key.startsWith('accept-') && session.route[key]),
|
|
15
|
+
supports = routes.map((route) => [this.#normalize(route), route])
|
|
16
|
+
|
|
17
|
+
for(let accepted of accepts)
|
|
18
|
+
{
|
|
19
|
+
accepted = accepted.split(';')[0].split('*')[0]
|
|
20
|
+
|
|
21
|
+
for(let [supported, route] in supports)
|
|
22
|
+
{
|
|
23
|
+
supported = supported.split('*')[0]
|
|
24
|
+
|
|
25
|
+
if(supported.startsWith(accepted)
|
|
26
|
+
|| accepted.startsWith(supported))
|
|
27
|
+
{
|
|
28
|
+
const
|
|
29
|
+
dispatcher = session.route[route],
|
|
30
|
+
dispatchers = Array.isArray(dispatcher) ? dispatcher : [dispatcher],
|
|
31
|
+
uniqueList = dispatchers.filter((item) => false === session.chain.dispatchers.includes(item))
|
|
32
|
+
|
|
33
|
+
// insert the forward routed dispatcher(s) after the current dispatcher in the chain
|
|
34
|
+
// for the dispatcher chain iterator to dispatch it/them next
|
|
35
|
+
session.chain.dispatchers.splice(session.chain.index, 0, ...uniqueList)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const
|
|
42
|
+
allowed = supports.map(([ supported ]) => supported),
|
|
43
|
+
error = new Error(`The requested resource "${request.method} ${request.url}" can not be delivered in requested header accept media types: ${this.#listFormat.format(accepts) || 'none are defined'}`)
|
|
44
|
+
|
|
45
|
+
error.code = 'E_HTTP_SERVER_MIDDLEWARE_ACCEPT_HEADER_NO_MATCHING_DISPATCHER'
|
|
46
|
+
error.status = 406
|
|
47
|
+
error.headers = { accept:allowed.join(',') }
|
|
48
|
+
error.cause = `Supported accept header media types are: ${this.#listFormat.format(allowed) || 'none are defined'}`
|
|
49
|
+
|
|
50
|
+
session.abortion.abort(error)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does not validate headers, just assumes that the body is a JSON string
|
|
3
|
+
*
|
|
4
|
+
* @memberof @superhero/http-server:middleware/upstream/header/content-type/application
|
|
5
|
+
*/
|
|
6
|
+
export default new class ContentTypeApplicationJsonHeaderUpstreamMiddleware
|
|
7
|
+
{
|
|
8
|
+
async dispatch(request, session)
|
|
9
|
+
{
|
|
10
|
+
const body = await request.body
|
|
11
|
+
|
|
12
|
+
if(body)
|
|
13
|
+
{
|
|
14
|
+
try
|
|
15
|
+
{
|
|
16
|
+
request.body = JSON.parse(body)
|
|
17
|
+
}
|
|
18
|
+
catch(reason)
|
|
19
|
+
{
|
|
20
|
+
const error = new Error('The body is not a valid JSON string')
|
|
21
|
+
error.code = 'E_HTTP_SERVER_MIDDLEWARE_CONTENT_TYPE_APPLICATION_JSON_INVALID_BODY'
|
|
22
|
+
error.status = 400
|
|
23
|
+
error.cause = 'The buffered body could not be parsed as a JSON string'
|
|
24
|
+
|
|
25
|
+
session.abortion.abort(error)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @memberof @superhero/http-server:middleware/upstream/header
|
|
3
|
+
*/
|
|
4
|
+
export default new class ContentTypeHeaderUpstreamMiddleware
|
|
5
|
+
{
|
|
6
|
+
#listFormat = new Intl.ListFormat('en', { style:'long', type:'disjunction' })
|
|
7
|
+
|
|
8
|
+
dispatch(request, session)
|
|
9
|
+
{
|
|
10
|
+
const
|
|
11
|
+
contentType = request.headers['content-type']?.toLowerCase().split(';')[0].split('*')[0].trim(),
|
|
12
|
+
routes = Object.keys(session.route).filter((key) => key.startsWith('content-type-') && session.route[key]),
|
|
13
|
+
supports = routes.map((route) => [route.replace('content-type-', '').trim(), route])
|
|
14
|
+
|
|
15
|
+
for(let [supported, route] in supports)
|
|
16
|
+
{
|
|
17
|
+
supported = supported.split('*')[0]
|
|
18
|
+
|
|
19
|
+
if(supported.startsWith(contentType)
|
|
20
|
+
|| contentType.startsWith(supported))
|
|
21
|
+
{
|
|
22
|
+
const
|
|
23
|
+
dispatcher = session.route[route],
|
|
24
|
+
dispatchers = Array.isArray(dispatcher) ? dispatcher : [dispatcher],
|
|
25
|
+
uniqueList = dispatchers.filter((item) => false === session.chain.dispatchers.includes(item))
|
|
26
|
+
|
|
27
|
+
// insert the forward routed dispatcher(s) after the current dispatcher in the chain
|
|
28
|
+
// for the dispatcher chain iterator to dispatch it/them next
|
|
29
|
+
session.chain.dispatchers.splice(session.chain.index, 0, ...uniqueList)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const
|
|
35
|
+
allowed = supports.map(([ supported ]) => supported),
|
|
36
|
+
error = new Error(`The requested resource "${request.method} ${request.url}" does not support content-type "${request.headers['content-type']}"`)
|
|
37
|
+
|
|
38
|
+
error.code = 'E_HTTP_SERVER_MIDDLEWARE_CONTENT_TYPE_NO_MATCHING_DISPATCHER'
|
|
39
|
+
error.status = 415
|
|
40
|
+
error.headers = { accept:allowed.join(',') }
|
|
41
|
+
error.cause = `Supported content-type headers are: ${this.#listFormat.format(allowed) || 'none are defined'}`
|
|
42
|
+
|
|
43
|
+
session.abortion.abort(error)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @memberof @superhero/http-server:middleware/upstream
|
|
3
|
+
*/
|
|
4
|
+
export default new class MethodUpstreamMiddleware
|
|
5
|
+
{
|
|
6
|
+
#listFormat = new Intl.ListFormat('en', { style:'long', type:'disjunction' })
|
|
7
|
+
|
|
8
|
+
dispatch(request, session)
|
|
9
|
+
{
|
|
10
|
+
const
|
|
11
|
+
method = request.method.toLowerCase(),
|
|
12
|
+
dispatcher = session.route['method-' + method] || session.route['method-*']
|
|
13
|
+
|
|
14
|
+
if(dispatcher)
|
|
15
|
+
{
|
|
16
|
+
const
|
|
17
|
+
dispatchers = Array.isArray(dispatcher) ? dispatcher : [dispatcher],
|
|
18
|
+
uniqueList = dispatchers.filter((item) => false === session.chain.dispatchers.includes(item))
|
|
19
|
+
|
|
20
|
+
// insert the forward routed dispatcher(s) after the current dispatcher in the chain
|
|
21
|
+
// for the dispatcher chain iterator to dispatch it/them next
|
|
22
|
+
session.chain.dispatchers.splice(session.chain.index, 0, ...uniqueList)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const
|
|
27
|
+
supports = Object.keys(session.route).filter((key) => key.startsWith('method-')),
|
|
28
|
+
allowed = supports.map((supported) => supported.replace('method-', '').toUpperCase()).sort(),
|
|
29
|
+
error = new Error(`The requested resource "${request.url}" does not support method "${request.method}"`)
|
|
30
|
+
|
|
31
|
+
error.code = 'E_HTTP_SERVER_MIDDLEWARE_METHOD_NO_MATCHING_DISPATCHER'
|
|
32
|
+
error.status = 405
|
|
33
|
+
error.headers = { allow:allowed.join(',') }
|
|
34
|
+
error.cause = `Supported methods are: ${this.#listFormat.format(allowed) || 'none are defined'}`
|
|
35
|
+
|
|
36
|
+
session.abortion.abort(error)
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@superhero/http-server",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "HTTP(S) server component supporting both HTTP 1.1 and HTTP 2.0",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"http server",
|
|
7
|
+
"http2 server",
|
|
8
|
+
"http2",
|
|
9
|
+
"http 2.0",
|
|
10
|
+
"http 1.1",
|
|
11
|
+
"https"
|
|
12
|
+
],
|
|
13
|
+
"main": "index.js",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.js",
|
|
18
|
+
"./view": "./view.js",
|
|
19
|
+
"./*/*/*": "./*/*/*.js",
|
|
20
|
+
"./*/*/*/*": "./*/*/*/*.js",
|
|
21
|
+
"./*/*/*/*/*": "./*/*/*/*/*.js",
|
|
22
|
+
"./*/*/*/*/*/*": "./*/*/*/*/*/*.js"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@superhero/router": "^4.0.5"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@superhero/locator": "^4.1.1",
|
|
29
|
+
"@superhero/http-request": "^4.0.9"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --trace-warnings --test --experimental-test-coverage"
|
|
33
|
+
},
|
|
34
|
+
"author": {
|
|
35
|
+
"name": "Erik Landvall",
|
|
36
|
+
"email": "erik@landvall.se"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/superhero/http-server"
|
|
41
|
+
}
|
|
42
|
+
}
|