@johntalton/http-core 1.0.1 → 1.0.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@johntalton/http-core",
3
3
  "type": "module",
4
- "version": "1.0.1",
4
+ "version": "1.0.3",
5
5
  "license": "MIT",
6
6
  "exports": {
7
7
  ".": "./src/index.js"
@@ -15,7 +15,7 @@
15
15
  "scripts": {
16
16
  },
17
17
  "dependencies": {
18
- "@johntalton/http-util": "^5.0.0",
18
+ "@johntalton/http-util": "^5.1.2",
19
19
  "@johntalton/sse-util": "^1.0.0"
20
20
  }
21
21
  }
package/src/epilogue.js CHANGED
@@ -1,5 +1,5 @@
1
- import { Response } from '@johntalton/http-util/response/object'
2
1
  import { MIME_TYPE_JSON } from '@johntalton/http-util/headers'
2
+ import { Response } from '@johntalton/http-util/response/object'
3
3
  import { ServerSentEvents } from '@johntalton/sse-util'
4
4
 
5
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
@@ -34,9 +34,10 @@ function addSSEPortHandler(stream, port, streamId, shutdownSignal) {
34
34
  port.onmessage = message => {
35
35
  const { data } = message
36
36
  console.log('sending sse data', streamId, data)
37
- // ServerSentEvents.messageToEventStreamLines(data)
38
- ServerSentEvents.lineGen(data)
39
- .forEach(line => stream.write(line))
37
+
38
+ for(const line of ServerSentEvents.lineGen(data)) {
39
+ stream.write(line)
40
+ }
40
41
  }
41
42
  }
42
43
 
@@ -53,17 +54,18 @@ export function epilogue(state) {
53
54
  case 'trace': { Response.trace(stream, state.method, state.url, state.headers, meta) } break
54
55
  //
55
56
  case 'preflight': { Response.preflight(stream, state.methods, state.supportedQueryTypes, undefined, meta) } break
56
- // case 'no-content': { Response.noContent(stream, state.etag, meta)} break
57
+ case 'no-content': { Response.noContent(stream, state.etag, meta)} break
57
58
  // case 'accepted': { Response.accepted(stream, meta) } break
58
59
  case 'created': { Response.created(stream, new URL(state.location, meta.origin), state.etag, meta) } break
59
60
  case 'not-modified': { Response.notModified(stream, state.etag, state.age, { priv: true, maxAge: 60 }, meta) } break
60
61
 
61
62
  //
62
63
  // case 'multiple-choices': { Response.multipleChoices(stream, meta) } break
63
- // case 'gone': { Response.gone(stream, meta) } break
64
- // case 'moved-permanently': { Response.movedPermanently(stream, state.location, meta) } break
65
- // case 'see-other': { Response.seeOther(stream, state.location, meta) } break
66
- // case 'temporary-redirect': { Response.temporaryRedirect(stream, state.location, meta) } break
64
+ case 'gone': { Response.gone(stream, meta) } break
65
+ case 'moved-permanently': { Response.movedPermanently(stream, state.location, meta) } break
66
+ case 'see-other': { Response.seeOther(stream, state.location, meta) } break
67
+ case 'temporary-redirect': { Response.temporaryRedirect(stream, state.location, meta) } break
68
+ case 'permanent-redirect': { Response.permanentRedirect(stream, state.location, meta) } break
67
69
 
68
70
  //
69
71
  case '404': { Response.notFound(stream, state.message, meta) } break
@@ -74,13 +76,14 @@ export function epilogue(state) {
74
76
  case 'unprocessable': { Response.unprocessable(stream, meta) } break
75
77
  case 'precondition-failed': { Response.preconditionFailed(stream, meta) } break
76
78
  case 'not-satisfiable': { Response.rangeNotSatisfiable(stream, { size: state.contentLength }, meta) } break
77
- // case 'content-too-large': { Response.contentTooLarge(stream, meta) } break
78
- // case 'insufficient-storage': { Response.insufficientStorage(stream, meta) } break
79
- // case 'too-many-requests': { Response.tooManyRequests(stream, state.limit, state.policies, meta) } break
80
- // case 'unauthorized': { Response.unauthorized(stream, meta) } break
79
+ case 'content-too-large': { Response.contentTooLarge(stream, meta) } break
80
+ case 'insufficient-storage': { Response.insufficientStorage(stream, meta) } break
81
+ case 'too-many-requests': { Response.tooManyRequests(stream, state.limit, state.policies, meta) } break
82
+ case 'unauthorized': { Response.unauthorized(stream, state.challenge, meta) } break
83
+ case 'forbidden': { Response.forbidden(stream, meta) } break
81
84
  case 'unavailable': { Response.unavailable(stream, state.message, state.retryAfter, meta)} break
82
85
  case 'not-implemented': { Response.notImplemented(stream, state.message, meta)} break
83
- // case 'timeout': { Response.timeout(stream, meta) } break
86
+ case 'timeout': { Response.timeout(stream, meta) } break
84
87
 
85
88
  //
86
89
  case 'sse': {
package/src/index.js CHANGED
@@ -1,11 +1,12 @@
1
- import http2 from 'node:http2'
2
- import fs from 'node:fs'
1
+ /** biome-ignore-all lint/nursery/noExcessiveLinesPerFile: legacy */
3
2
  import crypto from 'node:crypto'
3
+ import fs from 'node:fs'
4
+ import http2 from 'node:http2'
4
5
 
5
6
  import { HTTP_METHOD_QUERY } from '@johntalton/http-util/response'
6
7
 
7
- import { preamble } from './preamble.js'
8
8
  import { epilogue } from './epilogue.js'
9
+ import { preamble } from './preamble.js'
9
10
 
10
11
  const {
11
12
  HTTP2_METHOD_GET,
@@ -18,6 +19,12 @@ const {
18
19
  HTTP2_METHOD_TRACE
19
20
  } = http2.constants
20
21
 
22
+ const {
23
+ SSL_OP_NO_TLSv1,
24
+ SSL_OP_NO_TLSv1_1,
25
+ SSL_OP_NO_TLSv1_2,
26
+ } = crypto.constants
27
+
21
28
  export const KNOWN_METHODS = [
22
29
  HTTP2_METHOD_GET,
23
30
  HTTP2_METHOD_HEAD,
@@ -35,14 +42,46 @@ export const KNOWN_METHODS = [
35
42
 
36
43
  /** @import { Metadata } from '@johntalton/http-util/response' */
37
44
  /** @import { BodyFuture } from '@johntalton/http-util/body' */
38
- /** @import { EtagItem, IMFFixDate, ContentRangeDirective } from '@johntalton/http-util/headers' */
45
+ /** @import { EtagItem, IMFFixDate, ContentRangeDirective, RateLimitPolicyInfo, RateLimitInfo, ChallengeItem } from '@johntalton/http-util/headers' */
39
46
  /** @import { SendBody } from '@johntalton/http-util/response' */
40
47
 
41
48
  /** @typedef {(state: RouteRequest|RouteAction) => Promise<RouteAction>} Router */
42
49
 
43
50
  /** @typedef {'request'} RouteTypeRequest */
44
- /** @typedef {'partial-bytes'|'bytes'|'json'|'404'|'sse'|'error'|'preflight'|'not-allowed'|'trace'|'created'|'unsupported-media'|'not-modified'|'precondition-failed'|'unprocessable'|'not-acceptable'|'conflict'|'not-implemented'|'unavailable'|'not-satisfiable'} RouteType */
45
- /** @typedef {'GET'|'HEAD'|'POST'|'PUT'|'OPTIONS'|'DELETE'|'TRACE'} RouteMethod */
51
+ /** @typedef {
52
+ 'partial-bytes' |
53
+ 'bytes' |
54
+ 'json' |
55
+ '404' |
56
+ 'sse' |
57
+ 'error' |
58
+ 'preflight' |
59
+ 'not-allowed' |
60
+ 'trace' |
61
+ 'created' |
62
+ 'unsupported-media' |
63
+ 'not-modified' |
64
+ 'precondition-failed' |
65
+ 'unprocessable' |
66
+ 'not-acceptable' |
67
+ 'conflict' |
68
+ 'not-implemented' |
69
+ 'unavailable' |
70
+ 'not-satisfiable' |
71
+ 'see-other' |
72
+ 'temporary-redirect' |
73
+ 'permanent-redirect' |
74
+ 'moved-permanently' |
75
+ 'gone' |
76
+ 'no-content' |
77
+ 'content-too-large' |
78
+ 'insufficient-storage' |
79
+ 'too-many-requests' |
80
+ 'unauthorized' |
81
+ 'forbidden' |
82
+ 'timeout'
83
+ } RouteType */
84
+ /** @typedef {'GET'|'HEAD'|'POST'|'PUT'|'PATCH'|'OPTIONS'|'DELETE'|'TRACE'|'QUERY'} RouteMethod */
46
85
 
47
86
  /** @typedef {string & { readonly _brand: 'sid' }} StreamID */
48
87
 
@@ -271,6 +310,85 @@ export const KNOWN_METHODS = [
271
310
  */
272
311
  /** @typedef {RouteBase & RouteNotSatisfiableBase} RouteNotSatisfiable */
273
312
 
313
+ /**
314
+ * @typedef {Object} RouteSeeOtherBase
315
+ * @property {'see-other'} type
316
+ * @property {URL} location
317
+ */
318
+ /** @typedef {RouteBase & RouteSeeOtherBase} RouteSeeOther */
319
+
320
+ /**
321
+ * @typedef {Object} RouteTemporaryRedirectBase
322
+ * @property {'temporary-redirect'} type
323
+ * @property {URL} location
324
+ */
325
+ /** @typedef {RouteBase & RouteTemporaryRedirectBase} RouteTemporaryRedirect */
326
+
327
+ /**
328
+ * @typedef {Object} RoutePermanentRedirectBase
329
+ * @property {'permanent-redirect'} type
330
+ * @property {URL} location
331
+ */
332
+ /** @typedef {RouteBase & RoutePermanentRedirectBase} RoutePermanentRedirect */
333
+
334
+ /**
335
+ * @typedef {Object} RouteMovedPermanentlyBase
336
+ * @property {'moved-permanently'} type
337
+ * @property {URL} location
338
+ */
339
+ /** @typedef {RouteBase & RouteMovedPermanentlyBase} RouteMovedPermanently */
340
+
341
+ /**
342
+ * @typedef {Object} RouteNoContentBase
343
+ * @property {'no-content'} type
344
+ * @property {EtagItem|undefined} [etag]
345
+ */
346
+ /** @typedef {RouteBase & RouteNoContentBase} RouteNoContent */
347
+
348
+ /**
349
+ * @typedef {Object} RouteGoneBase
350
+ * @property {'gone'} type
351
+ */
352
+ /** @typedef {RouteBase & RouteGoneBase} RouteGone */
353
+
354
+ /**
355
+ * @typedef {Object} RouteContentTooLargeBase
356
+ * @property {'content-too-large'} type
357
+ */
358
+ /** @typedef {RouteBase & RouteContentTooLargeBase} RouteContentTooLarge */
359
+
360
+ /**
361
+ * @typedef {Object} RouteInsufficientStorageBase
362
+ * @property {'insufficient-storage'} type
363
+ */
364
+ /** @typedef {RouteBase & RouteInsufficientStorageBase} RouteInsufficientStorage */
365
+
366
+ /**
367
+ * @typedef {Object} RouteTooManyRequestsBase
368
+ * @property {'too-many-requests'} type
369
+ * @property {RateLimitInfo} limit
370
+ * @property {Array<RateLimitPolicyInfo>} policies
371
+ */
372
+ /** @typedef {RouteBase & RouteTooManyRequestsBase} RouteTooManyRequests */
373
+
374
+ /**
375
+ * @typedef {Object} RouteUnauthorizedBase
376
+ * @property {'unauthorized'} type
377
+ * @property {Array<ChallengeItem>} challenge
378
+ */
379
+ /** @typedef {RouteBase & RouteUnauthorizedBase} RouteUnauthorized */
380
+
381
+ /**
382
+ * @typedef {Object} RouteForbiddenBase
383
+ * @property {'forbidden'} type
384
+ */
385
+ /** @typedef {RouteBase & RouteForbiddenBase} RouteForbidden */
386
+
387
+ /**
388
+ * @typedef {Object} RouteTimeoutBase
389
+ * @property {'timeout'} type
390
+ */
391
+ /** @typedef {RouteBase & RouteTimeoutBase} RouteTimeout */
274
392
 
275
393
  /**
276
394
  * @typedef {Object} RouteSSEBase
@@ -301,7 +419,19 @@ export const KNOWN_METHODS = [
301
419
  RouteNotImplemented |
302
420
  RouteUnavailable |
303
421
  RoutePartialBytes |
304
- RouteNotSatisfiable
422
+ RouteNotSatisfiable |
423
+ RouteSeeOther |
424
+ RouteTemporaryRedirect |
425
+ RoutePermanentRedirect |
426
+ RouteMovedPermanently |
427
+ RouteNoContent |
428
+ RouteGone |
429
+ RouteContentTooLarge |
430
+ RouteInsufficientStorage |
431
+ RouteTooManyRequests |
432
+ RouteUnauthorized |
433
+ RouteForbidden |
434
+ RouteTimeout
305
435
  } RouteAction */
306
436
 
307
437
  /** @typedef {Record<string, string|undefined>} RouteMatches */
@@ -311,7 +441,7 @@ export const KNOWN_METHODS = [
311
441
  * @param {Http2Stream} stream
312
442
  * @returns {stream is ServerHttp2Stream}
313
443
  */
314
- function isServerStream(stream) {
444
+ export function isServerStream(stream) {
315
445
  if(stream === null) { return false }
316
446
  return true
317
447
  }
@@ -347,19 +477,19 @@ export function isValidMethod(method) {
347
477
  */
348
478
  export function closeCodeToString(rstCode) {
349
479
  if(rstCode === http2.constants.NGHTTP2_NO_ERROR) { return '(No Error)' }
350
- else if(rstCode === http2.constants.NGHTTP2_PROTOCOL_ERROR) { return '(Protocol Error)' }
351
- else if(rstCode === http2.constants.NGHTTP2_INTERNAL_ERROR) { return '(Internal Error)' }
352
- else if(rstCode === http2.constants.NGHTTP2_FLOW_CONTROL_ERROR) { return '(Flow Control Error)' }
353
- else if(rstCode === http2.constants.NGHTTP2_SETTINGS_TIMEOUT) { return '(Settings Timeout)' }
354
- else if(rstCode === http2.constants.NGHTTP2_STREAM_CLOSED) { return '(Closed)' }
355
- else if(rstCode === http2.constants.NGHTTP2_FRAME_SIZE_ERROR) { return '(Frame Size Error)' }
356
- else if(rstCode === http2.constants.NGHTTP2_REFUSED_STREAM) { return '(Refused)' }
357
- else if(rstCode === http2.constants.NGHTTP2_CANCEL) { return '(Cancel)' }
358
- else if(rstCode === http2.constants.NGHTTP2_COMPRESSION_ERROR) { return '(Compression Error)' }
359
- else if(rstCode === http2.constants.NGHTTP2_CONNECT_ERROR) { return '(Connect Error)' }
360
- else if(rstCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM) { return '(Chill)' }
361
- else if(rstCode === http2.constants.NGHTTP2_INADEQUATE_SECURITY) { return '(Inadequate Security)' }
362
- else if(rstCode === http2.constants.NGHTTP2_HTTP_1_1_REQUIRED) { return '(HTTP 1.1 Requested)' }
480
+ if(rstCode === http2.constants.NGHTTP2_PROTOCOL_ERROR) { return '(Protocol Error)' }
481
+ if(rstCode === http2.constants.NGHTTP2_INTERNAL_ERROR) { return '(Internal Error)' }
482
+ if(rstCode === http2.constants.NGHTTP2_FLOW_CONTROL_ERROR) { return '(Flow Control Error)' }
483
+ if(rstCode === http2.constants.NGHTTP2_SETTINGS_TIMEOUT) { return '(Settings Timeout)' }
484
+ if(rstCode === http2.constants.NGHTTP2_STREAM_CLOSED) { return '(Closed)' }
485
+ if(rstCode === http2.constants.NGHTTP2_FRAME_SIZE_ERROR) { return '(Frame Size Error)' }
486
+ if(rstCode === http2.constants.NGHTTP2_REFUSED_STREAM) { return '(Refused)' }
487
+ if(rstCode === http2.constants.NGHTTP2_CANCEL) { return '(Cancel)' }
488
+ if(rstCode === http2.constants.NGHTTP2_COMPRESSION_ERROR) { return '(Compression Error)' }
489
+ if(rstCode === http2.constants.NGHTTP2_CONNECT_ERROR) { return '(Connect Error)' }
490
+ if(rstCode === http2.constants.NGHTTP2_ENHANCE_YOUR_CALM) { return '(Chill)' }
491
+ if(rstCode === http2.constants.NGHTTP2_INADEQUATE_SECURITY) { return '(Inadequate Security)' }
492
+ if(rstCode === http2.constants.NGHTTP2_HTTP_1_1_REQUIRED) { return '(HTTP 1.1 Requested)' }
363
493
 
364
494
  return `(${rstCode})`
365
495
  }
@@ -372,16 +502,10 @@ export const REQUEST_ID_SIZE = 5
372
502
  export function requestId() {
373
503
  const buffer = new Uint8Array(REQUEST_ID_SIZE)
374
504
  crypto.getRandomValues(buffer)
375
- // @ts-ignore
505
+ // @ts-expect-error
376
506
  return buffer.toHex()
377
507
  }
378
508
 
379
- const {
380
- SSL_OP_NO_TLSv1,
381
- SSL_OP_NO_TLSv1_1,
382
- SSL_OP_NO_TLSv1_2,
383
- } = crypto.constants
384
-
385
509
  /**
386
510
  * @typedef {Object} H2CoreOptions
387
511
  * @property {Config} config
package/src/preamble.js CHANGED
@@ -3,24 +3,30 @@ import { TLSSocket } from 'node:tls'
3
3
 
4
4
  import { requestBody } from '@johntalton/http-util/body'
5
5
  import {
6
- MIME_TYPE_JSON,
7
- MIME_TYPE_TEXT,
8
- MIME_TYPE_XML,
9
- MIME_TYPE_EVENT_STREAM,
10
- MIME_TYPE_MESSAGE_HTTP,
11
- parseContentType,
12
-
13
6
  Accept,
14
7
  AcceptEncoding,
15
8
  AcceptLanguage,
16
9
 
17
- Forwarded,
10
+ Conditional,
11
+ ETag,
12
+
18
13
  FORWARDED_KEY_FOR,
14
+ Forwarded,
19
15
  KNOWN_FORWARDED_KEYS,
20
- Conditional,
21
- ETag
16
+
17
+ MIME_TYPE_EVENT_STREAM,
18
+ MIME_TYPE_JSON,
19
+ MIME_TYPE_MESSAGE_HTTP,
20
+ MIME_TYPE_TEXT,
21
+ MIME_TYPE_XML,
22
+ parseContentType
22
23
  } from '@johntalton/http-util/headers'
23
- import { ENCODER_MAP, HTTP_HEADER_FORWARDED, HTTP_HEADER_ORIGIN } from '@johntalton/http-util/response'
24
+ import {
25
+ ENCODER_MAP,
26
+ HTTP_HEADER_FORWARDED,
27
+ HTTP_HEADER_ORIGIN
28
+ } from '@johntalton/http-util/response'
29
+
24
30
  import { isValidHeader, isValidLikeHeader, isValidMethod } from './index.js'
25
31
 
26
32
  /** @import { ServerHttp2Stream, IncomingHttpHeaders } from 'node:http2' */
@@ -29,8 +35,8 @@ import { isValidHeader, isValidLikeHeader, isValidMethod } from './index.js'
29
35
  const { HTTP2_METHOD_OPTIONS, HTTP2_METHOD_TRACE } = http2.constants
30
36
 
31
37
  const {
32
- HTTP2_HEADER_METHOD,
33
38
  HTTP2_HEADER_AUTHORITY,
39
+ HTTP2_HEADER_METHOD,
34
40
  HTTP2_HEADER_SCHEME,
35
41
  HTTP2_HEADER_PATH,
36
42
  HTTP2_HEADER_AUTHORIZATION,
@@ -73,8 +79,10 @@ const ALLOWED_ORIGINS = (process.env['ALLOWED_ORIGINS'] ?? '').split(',').map(s
73
79
 
74
80
  const ALLOW_TRACE = process.env['ALLOW_TRACE'] === 'true'
75
81
 
76
- const BODY_TIMEOUT_SEC = 2 * 1000
77
- const BODY_BYTE_LENGTH = 1000 * 1000
82
+ const MSEC_PER_SEC = 1000
83
+ const BODY_TIMEOUT_MSEC = 2 * MSEC_PER_SEC
84
+ const BYTE_PER_K = 1024
85
+ const BODY_BYTE_LENGTH = BYTE_PER_K * BYTE_PER_K
78
86
 
79
87
  // const ipRateLimitStore = new Map()
80
88
  // const ipRateLimitPolicy = {
@@ -139,10 +147,8 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
139
147
  // const secFetchDest = header[HTTP_HEADER_SEC_FETCH_DEST]
140
148
 
141
149
  //
142
- const allowedOrigin = (origin !== undefined) ?
143
- ((ALLOWED_ORIGINS.includes(origin) || ALLOWED_ORIGINS.includes('*')) ?
144
- (URL.canParse(origin) ?
145
- origin : undefined) : undefined) : undefined
150
+ const allowedOrigin = (ALLOWED_ORIGINS.includes('*') || ((origin !== undefined) && URL.canParse(origin) && ALLOWED_ORIGINS.includes(origin))) ? origin : undefined
151
+
146
152
 
147
153
  /** @type {RouteRequest|RouteAction} */
148
154
  const state = {
@@ -249,7 +255,7 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
249
255
  //
250
256
  if(method === HTTP2_METHOD_TRACE) {
251
257
  if(!ALLOW_TRACE) { return { ...state, type: 'not-allowed', method, methods: [], url: requestUrl }}
252
- const maxForwardsValue = maxForwards !== undefined ? parseInt(maxForwards) : 0
258
+ const maxForwardsValue = maxForwards !== undefined ? Number.parseInt(maxForwards) : 0
253
259
  const preambleEnd = performance.now()
254
260
  state.meta.performance.push({ name: 'preamble-trace', duration: preambleEnd - preambleStart })
255
261
  if(acceptObject.type !== MIME_TYPE_MESSAGE_HTTP) { return { ...state, type: 'not-acceptable', acceptableMediaTypes: [ MIME_TYPE_MESSAGE_HTTP ] } }
@@ -259,14 +265,14 @@ export function preamble(config, streamId, stream, headers, servername, shutdown
259
265
  //
260
266
  // setup future body
261
267
  //
262
- const contentLength = fullContentLength === undefined ? undefined : parseInt(fullContentLength, 10)
268
+ const contentLength = fullContentLength === undefined ? undefined : Number.parseInt(fullContentLength, 10)
263
269
  const body = requestBody(stream, {
264
270
  byteLimit: BODY_BYTE_LENGTH,
265
271
  contentLength,
266
272
  contentType,
267
273
  signal: AbortSignal.any([
268
274
  shutdownSignal,
269
- AbortSignal.timeout(BODY_TIMEOUT_SEC)
275
+ AbortSignal.timeout(BODY_TIMEOUT_MSEC)
270
276
  ])
271
277
  })
272
278