@johntalton/http-util 2.0.0 → 2.0.2

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,16 +1,18 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
8
8
  "./headers": "./src/index.js",
9
9
  "./body": "./src/body.js",
10
- "./response": "./src/handle-stream-util.js"
10
+ "./response": "./src/response/index.js",
11
+ "./response/object": "./src/response/response.js"
11
12
  },
12
13
  "files": [
13
- "src/*.js"
14
+ "src/*.js",
15
+ "src/response/*.js"
14
16
  ],
15
17
  "repository": {
16
18
  "url": "https://github.com/johntalton/http-util"
@@ -0,0 +1,34 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
4
+ HTTP_HEADER_SERVER_TIMING,
5
+ ServerTiming
6
+ } from '../server-timing.js'
7
+
8
+ /** @import { ServerHttp2Stream } from 'node:http2' */
9
+ /** @import { Metadata } from './defs.js' */
10
+
11
+ const {
12
+ HTTP_STATUS_ACCEPTED
13
+ } = http2.constants
14
+
15
+ const {
16
+ HTTP2_HEADER_STATUS,
17
+ HTTP2_HEADER_SERVER,
18
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
19
+ } = http2.constants
20
+
21
+ /**
22
+ * @param {ServerHttp2Stream} stream
23
+ * @param {Metadata} meta
24
+ */
25
+ export function sendAccepted(stream, meta) {
26
+ stream.respond({
27
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
28
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_ACCEPTED,
29
+ [HTTP2_HEADER_SERVER]: meta.servername,
30
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
31
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance)
32
+ })
33
+ stream.end()
34
+ }
@@ -0,0 +1,37 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
4
+ HTTP_HEADER_SERVER_TIMING,
5
+ ServerTiming
6
+ } from '../server-timing.js'
7
+
8
+ /** @import { ServerHttp2Stream } from 'node:http2' */
9
+ /** @import { Metadata } from './defs.js' */
10
+
11
+ const {
12
+ HTTP_STATUS_CREATED
13
+ } = http2.constants
14
+
15
+ const {
16
+ HTTP2_HEADER_STATUS,
17
+ HTTP2_HEADER_SERVER,
18
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
19
+ HTTP2_HEADER_LOCATION
20
+ } = http2.constants
21
+
22
+ /**
23
+ * @param {ServerHttp2Stream} stream
24
+ * @param {URL} location
25
+ * @param {Metadata} meta
26
+ */
27
+ export function sendCreated(stream, location, meta) {
28
+ stream.respond({
29
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
30
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_CREATED,
31
+ [HTTP2_HEADER_SERVER]: meta.servername,
32
+ [HTTP2_HEADER_LOCATION]: location.href,
33
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
34
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
35
+ })
36
+ stream.end()
37
+ }
@@ -0,0 +1,34 @@
1
+
2
+ export const HTTP_HEADER_ORIGIN = 'origin'
3
+ export const HTTP_HEADER_USER_AGENT = 'user-agent'
4
+ export const HTTP_HEADER_FORWARDED = 'forwarded'
5
+ export const HTTP_HEADER_SEC_CH_UA = 'sec-ch-ua'
6
+ export const HTTP_HEADER_SEC_CH_PLATFORM = 'sec-ch-ua-platform'
7
+ export const HTTP_HEADER_SEC_CH_MOBILE = 'sec-ch-ua-mobile'
8
+ export const HTTP_HEADER_SEC_FETCH_SITE = 'sec-fetch-site'
9
+ export const HTTP_HEADER_SEC_FETCH_MODE = 'sec-fetch-mode'
10
+ export const HTTP_HEADER_SEC_FETCH_DEST = 'sec-fetch-dest'
11
+ export const HTTP_HEADER_ACCEPT_POST = 'accept-post'
12
+
13
+ export const DEFAULT_METHODS = [ 'HEAD', 'GET', 'POST', 'PATCH', 'DELETE' ]
14
+
15
+ export const HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE = 'access-control-max-age'
16
+ export const PREFLIGHT_AGE_SECONDS = '500'
17
+
18
+
19
+ /** @import { TimingsInfo } from '../server-timing.js' */
20
+
21
+ /**
22
+ * @typedef {Object} Metadata
23
+ * @property {Array<TimingsInfo>} performance
24
+ * @property {string|undefined} servername
25
+ * @property {string|undefined} origin
26
+ * @property {string|undefined} [etag]
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} SSEOptions
31
+ * @property {boolean} [active]
32
+ * @property {boolean} [bom]
33
+ */
34
+
@@ -0,0 +1,48 @@
1
+ import http2 from 'node:http2'
2
+
3
+ import {
4
+ CONTENT_TYPE_TEXT
5
+ } from '../content-type.js'
6
+
7
+ const {
8
+ HTTP2_HEADER_STATUS,
9
+ HTTP2_HEADER_CONTENT_TYPE,
10
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
11
+ HTTP2_HEADER_SERVER
12
+ } = http2.constants
13
+
14
+ const {
15
+ HTTP_STATUS_INTERNAL_SERVER_ERROR
16
+ } = http2.constants
17
+
18
+ /** @import { ServerHttp2Stream } from 'node:http2' */
19
+ /** @import { Metadata } from './defs.js' */
20
+
21
+ /**
22
+ * @param {ServerHttp2Stream} stream
23
+ * @param {string} message
24
+ * @param {Metadata} meta
25
+ */
26
+ export function sendError(stream, message, meta) {
27
+ console.error('500', message)
28
+
29
+ if(stream === undefined) { return }
30
+ if(stream.closed) { return }
31
+
32
+ if(!stream.headersSent) {
33
+ stream.respond({
34
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
35
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_INTERNAL_SERVER_ERROR,
36
+ [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_TEXT,
37
+ [HTTP2_HEADER_SERVER]: meta.servername
38
+ })
39
+ }
40
+
41
+ // protect against HEAD calls
42
+ if(stream.writable) {
43
+ if(message !== undefined) { stream.write(message) }
44
+ }
45
+
46
+ stream.end()
47
+ if(!stream.closed) { stream.close() }
48
+ }
@@ -0,0 +1,16 @@
1
+ export * from './defs.js'
2
+
3
+ export * from './accepted.js'
4
+ export * from './created.js'
5
+ export * from './error.js'
6
+ export * from './json.js'
7
+ export * from './not-acceptable.js'
8
+ export * from './not-allowed.js'
9
+ export * from './not-found.js'
10
+ export * from './not-modified.js'
11
+ export * from './preflight.js'
12
+ export * from './sse.js'
13
+ export * from './too-many-requests.js'
14
+ export * from './trace.js'
15
+ export * from './unauthorized.js'
16
+ export * from './unsupported-media.js'
@@ -0,0 +1,78 @@
1
+ import http2 from 'node:http2'
2
+ import { brotliCompressSync, deflateSync, gzipSync, zstdCompressSync } from 'node:zlib'
3
+ import { ServerTiming, HTTP_HEADER_SERVER_TIMING, HTTP_HEADER_TIMING_ALLOW_ORIGIN } from '../server-timing.js'
4
+ import {
5
+ CHARSET_UTF8,
6
+ CONTENT_TYPE_JSON
7
+ } from '../content-type.js'
8
+
9
+ /** @import { ServerHttp2Stream } from 'node:http2' */
10
+ /** @import { Metadata } from './defs.js' */
11
+
12
+ /** @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun */
13
+
14
+ const {
15
+ HTTP2_HEADER_STATUS,
16
+ HTTP2_HEADER_CONTENT_TYPE,
17
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
18
+ HTTP2_HEADER_SERVER,
19
+ HTTP2_HEADER_CONTENT_ENCODING,
20
+ HTTP2_HEADER_VARY,
21
+ HTTP2_HEADER_CACHE_CONTROL,
22
+ HTTP2_HEADER_ETAG
23
+ } = http2.constants
24
+
25
+ const {
26
+ HTTP_STATUS_OK
27
+ } = http2.constants
28
+
29
+ /** @type {Map<string, EncoderFun>} */
30
+ export const ENCODER_MAP = new Map([
31
+ [ 'br', (data, charset) => brotliCompressSync(Buffer.from(data, charset)) ],
32
+ [ 'gzip', (data, charset) => gzipSync(Buffer.from(data, charset)) ],
33
+ [ 'deflate', (data, charset) => deflateSync(Buffer.from(data, charset)) ],
34
+ [ 'zstd', (data, charset) => zstdCompressSync(Buffer.from(data, charset)) ]
35
+ ])
36
+
37
+ /**
38
+ * @param {ServerHttp2Stream} stream
39
+ * @param {Object} obj
40
+ * @param {string|undefined} encoding
41
+ * @param {Metadata} meta
42
+ */
43
+ export function sendJSON_Encoded(stream, obj, encoding, meta) {
44
+ if(stream.closed) { return }
45
+
46
+ const json = JSON.stringify(obj)
47
+
48
+ const useIdentity = encoding === 'identity'
49
+ const encoder = encoding !== undefined ? ENCODER_MAP.get(encoding) : undefined
50
+ const hasEncoder = encoder !== undefined
51
+ const actualEncoding = hasEncoder ? encoding : undefined
52
+
53
+ const encodeStart = performance.now()
54
+ const encodedData = hasEncoder && !useIdentity ? encoder(json, CHARSET_UTF8) : json
55
+ const encodeEnd = performance.now()
56
+
57
+ meta.performance.push(
58
+ { name: 'encode', duration: encodeEnd - encodeStart }
59
+ )
60
+
61
+ stream.respond({
62
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
63
+ [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
64
+ [HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
65
+ [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
66
+ [HTTP2_HEADER_CACHE_CONTROL]: 'private',
67
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
68
+ [HTTP2_HEADER_SERVER]: meta.servername,
69
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
70
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
71
+ [HTTP2_HEADER_ETAG]: `"${meta.etag}"`
72
+ // [HTTP2_HEADER_AGE]: age
73
+ })
74
+
75
+ // stream.write(encodedData)
76
+ stream.end(encodedData)
77
+ }
78
+
@@ -0,0 +1,46 @@
1
+ import http2 from 'node:http2'
2
+ import { MIME_TYPE_JSON } from '../content-type.js'
3
+ import {
4
+ HTTP_HEADER_SERVER_TIMING,
5
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
6
+ ServerTiming
7
+ } from '../server-timing.js'
8
+
9
+ /** @import { ServerHttp2Stream } from 'node:http2' */
10
+ /** @import { Metadata } from './defs.js' */
11
+
12
+ const {
13
+ HTTP_STATUS_NOT_ACCEPTABLE
14
+ } = http2.constants
15
+
16
+ const {
17
+ HTTP2_HEADER_STATUS,
18
+ HTTP2_HEADER_SERVER,
19
+ HTTP2_HEADER_CONTENT_TYPE,
20
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
21
+ } = http2.constants
22
+
23
+ /**
24
+ * @param {ServerHttp2Stream} stream
25
+ * @param {Array<string>|string} supportedTypes
26
+ * @param {Metadata} meta
27
+ */
28
+ export function sendNotAcceptable(stream, supportedTypes, meta) {
29
+ const supportedTypesList = Array.isArray(supportedTypes) ? supportedTypes : [ supportedTypes ]
30
+ const has = supportedTypesList.length !== 0
31
+
32
+ stream.respond({
33
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
34
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_NOT_ACCEPTABLE,
35
+ [HTTP2_HEADER_SERVER]: meta.servername,
36
+ [HTTP2_HEADER_CONTENT_TYPE]: has ? MIME_TYPE_JSON : undefined,
37
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
38
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
39
+ })
40
+
41
+ if(has) {
42
+ stream.write(JSON.stringify(supportedTypes))
43
+ }
44
+
45
+ stream.end()
46
+ }
@@ -0,0 +1,28 @@
1
+ import http2 from 'node:http2'
2
+
3
+ /** @import { ServerHttp2Stream } from 'node:http2' */
4
+ /** @import { Metadata } from './defs.js' */
5
+
6
+ const {
7
+ HTTP2_HEADER_STATUS,
8
+ HTTP2_HEADER_SERVER,
9
+ HTTP2_HEADER_ALLOW
10
+ } = http2.constants
11
+
12
+ const {
13
+ HTTP_STATUS_METHOD_NOT_ALLOWED
14
+ } = http2.constants
15
+
16
+ /**
17
+ * @param {ServerHttp2Stream} stream
18
+ * @param {Array<string>} methods
19
+ * @param {Metadata} meta
20
+ */
21
+ export function sendNotAllowed(stream, methods, meta) {
22
+ stream.respond({
23
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_METHOD_NOT_ALLOWED,
24
+ [HTTP2_HEADER_ALLOW]: methods.join(','),
25
+ [HTTP2_HEADER_SERVER]: meta.servername
26
+ })
27
+ stream.end()
28
+ }
@@ -0,0 +1,36 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ CONTENT_TYPE_TEXT
4
+ } from '../content-type.js'
5
+
6
+ /** @import { ServerHttp2Stream } from 'node:http2' */
7
+ /** @import { Metadata } from './defs.js' */
8
+
9
+ const {
10
+ HTTP2_HEADER_STATUS,
11
+ HTTP2_HEADER_CONTENT_TYPE,
12
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
13
+ HTTP2_HEADER_SERVER
14
+ } = http2.constants
15
+
16
+ const {
17
+ HTTP_STATUS_NOT_FOUND
18
+ } = http2.constants
19
+
20
+ /**
21
+ * @param {ServerHttp2Stream} stream
22
+ * @param {string} message
23
+ * @param {Metadata} meta
24
+ */
25
+ export function sendNotFound(stream, message, meta) {
26
+ console.log('404', message)
27
+ stream.respond({
28
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
29
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_NOT_FOUND,
30
+ [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_TEXT,
31
+ [HTTP2_HEADER_SERVER]: meta.servername
32
+ })
33
+
34
+ if(message !== undefined) { stream.write(message) }
35
+ stream.end()
36
+ }
@@ -0,0 +1,43 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
4
+ HTTP_HEADER_SERVER_TIMING,
5
+ ServerTiming
6
+ } from '../server-timing.js'
7
+
8
+ /** @import { ServerHttp2Stream } from 'node:http2' */
9
+ /** @import { Metadata } from './defs.js' */
10
+
11
+ const {
12
+ HTTP_STATUS_NOT_MODIFIED
13
+ } = http2.constants
14
+
15
+ const {
16
+ HTTP2_HEADER_STATUS,
17
+ HTTP2_HEADER_SERVER,
18
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
19
+ HTTP2_HEADER_AGE,
20
+ HTTP2_HEADER_ETAG,
21
+ HTTP2_HEADER_VARY,
22
+ HTTP2_HEADER_CACHE_CONTROL
23
+ } = http2.constants
24
+
25
+ /**
26
+ * @param {ServerHttp2Stream} stream
27
+ * @param {number} age
28
+ * @param {Metadata} meta
29
+ */
30
+ export function sendNotModified(stream, age, meta) {
31
+ stream.respond({
32
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
33
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_NOT_MODIFIED,
34
+ [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
35
+ [HTTP2_HEADER_CACHE_CONTROL]: 'private',
36
+ [HTTP2_HEADER_SERVER]: meta.servername,
37
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
38
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
39
+ [HTTP2_HEADER_ETAG]: `"${meta.etag}"`,
40
+ [HTTP2_HEADER_AGE]: age
41
+ })
42
+ stream.end()
43
+ }
@@ -0,0 +1,38 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
4
+ PREFLIGHT_AGE_SECONDS
5
+ } from './defs.js'
6
+
7
+ /** @import { ServerHttp2Stream } from 'node:http2' */
8
+ /** @import { Metadata } from './defs.js' */
9
+
10
+ const {
11
+ HTTP2_HEADER_STATUS,
12
+ HTTP2_HEADER_CONTENT_TYPE,
13
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
14
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS,
15
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
16
+ HTTP2_HEADER_SERVER
17
+ } = http2.constants
18
+
19
+ const {
20
+ HTTP_STATUS_OK
21
+ } = http2.constants
22
+
23
+ /**
24
+ * @param {ServerHttp2Stream} stream
25
+ * @param {Array<string>} methods
26
+ * @param {Metadata} meta
27
+ */
28
+ export function sendPreflight(stream, methods, meta) {
29
+ stream.respond({
30
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
31
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
32
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS]: methods.join(','),
33
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: ['Authorization', HTTP2_HEADER_CONTENT_TYPE].join(','),
34
+ [HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS,
35
+ [HTTP2_HEADER_SERVER]: meta.servername
36
+ })
37
+ stream.end()
38
+ }
@@ -0,0 +1,31 @@
1
+ import { sendAccepted } from './accepted.js'
2
+ import { sendCreated } from './created.js'
3
+ import { sendError } from './error.js'
4
+ import { sendJSON_Encoded } from './json.js'
5
+ import { sendNotAcceptable } from './not-acceptable.js'
6
+ import { sendNotAllowed } from './not-allowed.js'
7
+ import { sendNotFound } from './not-found.js'
8
+ import { sendNotModified } from './not-modified.js'
9
+ import { sendPreflight } from './preflight.js'
10
+ import { sendSSE } from './sse.js'
11
+ import { sendTooManyRequests } from './too-many-requests.js'
12
+ import { sendTrace } from './trace.js'
13
+ import { sendUnauthorized } from './unauthorized.js'
14
+ import { sendUnsupportedMediaType } from './unsupported-media.js'
15
+
16
+ export const Response = {
17
+ accepted: sendAccepted,
18
+ created: sendCreated,
19
+ error: sendError,
20
+ json: sendJSON_Encoded,
21
+ notAcceptable: sendNotAcceptable,
22
+ notAllowed: sendNotAllowed,
23
+ notFound: sendNotFound,
24
+ notModified: sendNotModified,
25
+ preflight: sendPreflight,
26
+ sse: sendSSE,
27
+ tooManyRequests: sendTooManyRequests,
28
+ trace: sendTrace,
29
+ unauthorized: sendUnauthorized,
30
+ unsupportedMediaType: sendUnsupportedMediaType
31
+ }
@@ -0,0 +1,57 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ SSE_MIME,
4
+ SSE_INACTIVE_STATUS_CODE,
5
+ SSE_BOM,
6
+ ENDING,
7
+ } from '@johntalton/sse-util'
8
+
9
+ /** @import { ServerHttp2Stream } from 'node:http2' */
10
+ /** @import { Metadata, SSEOptions } from './defs.js' */
11
+
12
+ const {
13
+ HTTP2_HEADER_STATUS,
14
+ HTTP2_HEADER_CONTENT_TYPE,
15
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
16
+ HTTP2_HEADER_SERVER
17
+ } = http2.constants
18
+
19
+ const {
20
+ HTTP_STATUS_OK,
21
+ HTTP_STATUS_NO_CONTENT
22
+ } = http2.constants
23
+
24
+ /**
25
+ * @param {ServerHttp2Stream} stream
26
+ * @param {SSEOptions & Metadata} meta
27
+ */
28
+ export function sendSSE(stream, meta) {
29
+ // stream.setTimeout(0)
30
+ // stream.session?.setTimeout(0)
31
+ // stream.session?.socket.setTimeout(0)
32
+ // stream.session.socket.setNoDelay(true)
33
+ // stream.session.socket.setKeepAlive(true)
34
+
35
+ // stream.on('close', () => console.log('SSE stream closed'))
36
+ // stream.on('aborted', () => console.log('SSE stream aborted'))
37
+
38
+ const activeStream = meta.active ?? true
39
+ const sendBOM = meta.bom ?? true
40
+
41
+ stream.respond({
42
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
43
+ [HTTP2_HEADER_CONTENT_TYPE]: SSE_MIME,
44
+ [HTTP2_HEADER_STATUS]: activeStream ? HTTP_STATUS_OK : HTTP_STATUS_NO_CONTENT, // SSE_INACTIVE_STATUS_CODE
45
+ // [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS]: 'true'
46
+ [HTTP2_HEADER_SERVER]: meta.servername
47
+ })
48
+
49
+ if(!activeStream) {
50
+ stream.end()
51
+ return
52
+ }
53
+
54
+ if(sendBOM) {
55
+ stream.write(SSE_BOM + ENDING.CRLF)
56
+ }
57
+ }
@@ -0,0 +1,43 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ HTTP_HEADER_RATE_LIMIT,
4
+ HTTP_HEADER_RATE_LIMIT_POLICY,
5
+ RateLimit,
6
+ RateLimitPolicy
7
+ } from '../rate-limit.js'
8
+
9
+ /** @import { ServerHttp2Stream } from 'node:http2' */
10
+ /** @import { Metadata } from './defs.js' */
11
+
12
+ const {
13
+ HTTP2_HEADER_STATUS,
14
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
15
+ HTTP2_HEADER_SERVER,
16
+ HTTP2_HEADER_RETRY_AFTER
17
+ } = http2.constants
18
+
19
+ const {
20
+ HTTP_STATUS_TOO_MANY_REQUESTS
21
+ } = http2.constants
22
+
23
+ /**
24
+ * @param {ServerHttp2Stream} stream
25
+ * @param {*} limitInfo
26
+ * @param {Array<any>} policies
27
+ * @param {Metadata} meta
28
+ */
29
+ export function sendTooManyRequests(stream, limitInfo, policies, meta) {
30
+ stream.respond({
31
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
32
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_TOO_MANY_REQUESTS,
33
+ [HTTP2_HEADER_SERVER]: meta.servername,
34
+
35
+ [HTTP2_HEADER_RETRY_AFTER]: limitInfo.retryAfterS,
36
+ [HTTP_HEADER_RATE_LIMIT]: RateLimit.from(limitInfo),
37
+ [HTTP_HEADER_RATE_LIMIT_POLICY]: RateLimitPolicy.from(...policies)
38
+ })
39
+
40
+ stream.write(`Retry After ${limitInfo.retryAfterS} Seconds`)
41
+ stream.end()
42
+ }
43
+
@@ -0,0 +1,63 @@
1
+ import http2 from 'node:http2'
2
+ import {
3
+ MIME_TYPE_MESSAGE_HTTP
4
+ } from '../content-type.js'
5
+ import {
6
+ HTTP_HEADER_SERVER_TIMING,
7
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
8
+ ServerTiming
9
+ } from '../server-timing.js'
10
+
11
+ /** @import { ServerHttp2Stream } from 'node:http2' */
12
+ /** @import { IncomingHttpHeaders } from 'node:http2' */
13
+ /** @import { Metadata } from './defs.js' */
14
+
15
+ const {
16
+ HTTP2_HEADER_STATUS,
17
+ HTTP2_HEADER_CONTENT_TYPE,
18
+ HTTP2_HEADER_SERVER
19
+ } = http2.constants
20
+
21
+ const {
22
+ HTTP_STATUS_OK
23
+ } = http2.constants
24
+
25
+ /**
26
+ * @param {ServerHttp2Stream} stream
27
+ * @param {string} method
28
+ * @param {URL} url
29
+ * @param {IncomingHttpHeaders} headers
30
+ * @param {Metadata} meta
31
+ */
32
+ export function sendTrace(stream, method, url, headers, meta) {
33
+ const FILTER_KEYS = [ 'authorization', 'cookie' ]
34
+ const HTTP_VERSION = new Map([
35
+ [ 'h2', 'HTTP/2' ],
36
+ [ 'h2c', 'HTTP/2'],
37
+ [ 'http/1.1', 'HTTP/1.1']
38
+ ])
39
+
40
+ const version = HTTP_VERSION.get(stream.session?.alpnProtocol ?? 'h2')
41
+
42
+ stream.respond({
43
+ [HTTP2_HEADER_CONTENT_TYPE]: MIME_TYPE_MESSAGE_HTTP,
44
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
45
+ [HTTP2_HEADER_SERVER]: meta.servername,
46
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
47
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
48
+ })
49
+
50
+ const reconstructed = [
51
+ `${method} ${url.pathname}${url.search} ${version}`,
52
+ Object.entries(headers)
53
+ .filter(([ key ]) => !key.startsWith(':'))
54
+ .filter(([ key ]) => !FILTER_KEYS.includes(key))
55
+ .map(([ key, value ]) => `${key}: ${value}`)
56
+ .join('\n'),
57
+ '\n'
58
+ ]
59
+ .join('\n')
60
+
61
+ stream.end(reconstructed)
62
+ }
63
+
@@ -0,0 +1,29 @@
1
+ import http2 from 'node:http2'
2
+
3
+ /** @import { ServerHttp2Stream } from 'node:http2' */
4
+ /** @import { Metadata } from './defs.js' */
5
+
6
+ const {
7
+ HTTP2_HEADER_STATUS,
8
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
9
+ HTTP2_HEADER_SERVER
10
+ } = http2.constants
11
+
12
+ const {
13
+ HTTP_STATUS_UNAUTHORIZED
14
+ } = http2.constants
15
+
16
+ /**
17
+ * @param {ServerHttp2Stream} stream
18
+ * @param {Metadata} meta
19
+ */
20
+ export function sendUnauthorized(stream, meta) {
21
+ console.log('Unauthorized')
22
+
23
+ stream.respond({
24
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
25
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_UNAUTHORIZED,
26
+ [HTTP2_HEADER_SERVER]: meta.servername
27
+ })
28
+ stream.end()
29
+ }
@@ -0,0 +1,40 @@
1
+ import http2 from 'node:http2'
2
+ import { HTTP_HEADER_ACCEPT_POST } from './defs.js'
3
+ import {
4
+ HTTP_HEADER_TIMING_ALLOW_ORIGIN,
5
+ HTTP_HEADER_SERVER_TIMING,
6
+ ServerTiming
7
+ } from '../server-timing.js'
8
+
9
+ /** @import { ServerHttp2Stream } from 'node:http2' */
10
+ /** @import { Metadata } from './defs.js' */
11
+
12
+ const {
13
+ HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE
14
+ } = http2.constants
15
+
16
+ const {
17
+ HTTP2_HEADER_STATUS,
18
+ HTTP2_HEADER_SERVER,
19
+ HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
20
+ } = http2.constants
21
+
22
+ /**
23
+ * @param {ServerHttp2Stream} stream
24
+ * @param {Array<string>|string} acceptableMediaType
25
+ * @param {Metadata} meta
26
+ */
27
+ export function sendUnsupportedMediaType(stream, acceptableMediaType, meta) {
28
+ const acceptable = Array.isArray(acceptableMediaType) ? acceptableMediaType : [ acceptableMediaType ]
29
+
30
+ stream.respond({
31
+ [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
32
+ [HTTP2_HEADER_STATUS]: HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE,
33
+ [HTTP_HEADER_ACCEPT_POST]: acceptable.join(','),
34
+ [HTTP2_HEADER_SERVER]: meta.servername,
35
+ [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
36
+ [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
37
+ })
38
+
39
+ stream.end()
40
+ }
@@ -1,324 +0,0 @@
1
- import http2 from 'node:http2'
2
- import { brotliCompressSync, deflateSync, gzipSync, zstdCompressSync } from 'node:zlib'
3
-
4
- import {
5
- SSE_MIME,
6
- SSE_INACTIVE_STATUS_CODE,
7
- SSE_BOM,
8
- ENDING,
9
- } from '@johntalton/sse-util'
10
-
11
- /** @import { IncomingHttpHeaders } from 'node:http2' */
12
-
13
- import {
14
- CHARSET_UTF8,
15
- CONTENT_TYPE_JSON,
16
- CONTENT_TYPE_TEXT,
17
- MIME_TYPE_MESSAGE_HTTP
18
- } from './content-type.js'
19
- import { ServerTiming, HTTP_HEADER_SERVER_TIMING, HTTP_HEADER_TIMING_ALLOW_ORIGIN } from './server-timing.js'
20
- import { HTTP_HEADER_RATE_LIMIT, HTTP_HEADER_RATE_LIMIT_POLICY, RateLimit, RateLimitPolicy } from './rate-limit.js'
21
-
22
- const {
23
- HTTP2_HEADER_STATUS,
24
- HTTP2_HEADER_CONTENT_TYPE,
25
- HTTP2_HEADER_CONTENT_ENCODING,
26
- HTTP2_HEADER_VARY,
27
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
28
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS,
29
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
30
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
31
- HTTP2_HEADER_SERVER,
32
- HTTP2_HEADER_RETRY_AFTER,
33
- HTTP2_HEADER_CACHE_CONTROL,
34
- HTTP2_HEADER_ETAG,
35
- HTTP2_HEADER_ALLOW
36
- } = http2.constants
37
-
38
- const {
39
- HTTP_STATUS_OK,
40
- HTTP_STATUS_NOT_FOUND,
41
- HTTP_STATUS_UNAUTHORIZED,
42
- HTTP_STATUS_NO_CONTENT,
43
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
44
- HTTP_STATUS_TOO_MANY_REQUESTS,
45
- HTTP_STATUS_METHOD_NOT_ALLOWED
46
- } = http2.constants
47
-
48
- export const HTTP_HEADER_ORIGIN = 'origin'
49
- export const HTTP_HEADER_USER_AGENT = 'user-agent'
50
- export const HTTP_HEADER_FORWARDED = 'forwarded'
51
- export const HTTP_HEADER_SEC_CH_UA = 'sec-ch-ua'
52
- export const HTTP_HEADER_SEC_CH_PLATFORM = 'sec-ch-ua-platform'
53
- export const HTTP_HEADER_SEC_CH_MOBILE = 'sec-ch-ua-mobile'
54
- export const HTTP_HEADER_SEC_FETCH_SITE = 'sec-fetch-site'
55
- export const HTTP_HEADER_SEC_FETCH_MODE = 'sec-fetch-mode'
56
- export const HTTP_HEADER_SEC_FETCH_DEST = 'sec-fetch-dest'
57
-
58
- export const DEFAULT_METHODS = [ 'HEAD', 'GET', 'POST', 'PATCH', 'DELETE' ]
59
-
60
- export const HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE = 'access-control-max-age'
61
- export const PREFLIGHT_AGE_SECONDS = '500'
62
-
63
- /**
64
- * @import { ServerHttp2Stream } from 'node:http2'
65
- */
66
-
67
- /**
68
- * @import { TimingsInfo } from './server-timing.js'
69
- */
70
-
71
- /**
72
- * @typedef {Object} Metadata
73
- * @property {Array<TimingsInfo>} performance
74
- * @property {string|undefined} servername
75
- * @property {string|undefined} origin
76
- * @property {string|undefined} etag
77
- */
78
-
79
- /**
80
- * @typedef {Object} SSEOptions
81
- * @property {boolean} [active]
82
- * @property {boolean} [bom]
83
- */
84
-
85
- /**
86
- * @param {ServerHttp2Stream} stream
87
- * @param {string} message
88
- * @param {Metadata} meta
89
- */
90
- export function sendError(stream, message, meta) {
91
- console.error('500', message)
92
-
93
- if(stream === undefined) { return }
94
- if(stream.closed) { return }
95
-
96
- if(!stream.headersSent) {
97
- stream.respond({
98
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
99
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_INTERNAL_SERVER_ERROR,
100
- [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_TEXT,
101
- [HTTP2_HEADER_SERVER]: meta.servername
102
- })
103
- }
104
-
105
- // protect against HEAD calls
106
- if(stream.writable) {
107
- if(message !== undefined) { stream.write(message) }
108
- }
109
-
110
- stream.end()
111
- if(!stream.closed) { stream.close() }
112
- }
113
-
114
- /**
115
- * @param {ServerHttp2Stream} stream
116
- * @param {Array<string>} methods
117
- * @param {Metadata} meta
118
- */
119
- export function sendPreflight(stream, methods, meta) {
120
- stream.respond({
121
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
122
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
123
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS]: methods.join(','),
124
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: ['Authorization', HTTP2_HEADER_CONTENT_TYPE].join(','),
125
- [HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS,
126
- [HTTP2_HEADER_SERVER]: meta.servername
127
- })
128
- stream.end()
129
- }
130
-
131
- /**
132
- * @param {ServerHttp2Stream} stream
133
- * @param {Array<string>} methods
134
- * @param {Metadata} meta
135
- */
136
- export function sendNowAllowed(stream, methods, meta) {
137
- stream.respond({
138
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_METHOD_NOT_ALLOWED,
139
- [HTTP2_HEADER_ALLOW]: methods.join(','),
140
- [HTTP2_HEADER_SERVER]: meta.servername
141
- })
142
- stream.end()
143
- }
144
-
145
- /**
146
- * @param {ServerHttp2Stream} stream
147
- * @param {Metadata} meta
148
- */
149
- export function sendUnauthorized(stream, meta) {
150
- console.log('Unauthorized')
151
-
152
- stream.respond({
153
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
154
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_UNAUTHORIZED,
155
- [HTTP2_HEADER_SERVER]: meta.servername
156
- })
157
- stream.end()
158
- }
159
-
160
- /**
161
- * @param {ServerHttp2Stream} stream
162
- * @param {string} message
163
- * @param {Metadata} meta
164
- */
165
- export function sendNotFound(stream, message, meta) {
166
- console.log('404', message)
167
- stream.respond({
168
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
169
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_NOT_FOUND,
170
- [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_TEXT,
171
- [HTTP2_HEADER_SERVER]: meta.servername
172
- })
173
-
174
- if(message !== undefined) { stream.write(message) }
175
- stream.end()
176
- }
177
-
178
- /**
179
- * @param {ServerHttp2Stream} stream
180
- * @param {*} limitInfo
181
- * @param {Array<any>} policies
182
- * @param {Metadata} meta
183
- */
184
- export function sendTooManyRequests(stream, limitInfo, policies, meta) {
185
- stream.respond({
186
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
187
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_TOO_MANY_REQUESTS,
188
- [HTTP2_HEADER_SERVER]: meta.servername,
189
-
190
- [HTTP2_HEADER_RETRY_AFTER]: limitInfo.retryAfterS,
191
- [HTTP_HEADER_RATE_LIMIT]: RateLimit.from(limitInfo),
192
- [HTTP_HEADER_RATE_LIMIT_POLICY]: RateLimitPolicy.from(...policies)
193
- })
194
-
195
- stream.write(`Retry After ${limitInfo.retryAfterS} Seconds`)
196
- stream.end()
197
- }
198
-
199
- /**
200
- * @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun
201
- */
202
-
203
- /** @type {Map<string, EncoderFun>} */
204
- export const ENCODER_MAP = new Map([
205
- [ 'br', (data, charset) => brotliCompressSync(Buffer.from(data, charset)) ],
206
- [ 'gzip', (data, charset) => gzipSync(Buffer.from(data, charset)) ],
207
- [ 'deflate', (data, charset) => deflateSync(Buffer.from(data, charset)) ],
208
- [ 'zstd', (data, charset) => zstdCompressSync(Buffer.from(data, charset)) ]
209
- ])
210
-
211
- /**
212
- * @param {ServerHttp2Stream} stream
213
- * @param {Object} obj
214
- * @param {string|undefined} encoding
215
- * @param {Metadata} meta
216
- */
217
- export function sendJSON_Encoded(stream, obj, encoding, meta) {
218
- if(stream.closed) { return }
219
-
220
- const json = JSON.stringify(obj)
221
-
222
- const useIdentity = encoding === 'identity'
223
- const encoder = encoding !== undefined ? ENCODER_MAP.get(encoding) : undefined
224
- const hasEncoder = encoder !== undefined
225
- const actualEncoding = hasEncoder ? encoding : undefined
226
-
227
- const encodeStart = performance.now()
228
- const encodedData = hasEncoder && !useIdentity ? encoder(json, CHARSET_UTF8) : json
229
- const encodeEnd = performance.now()
230
-
231
- meta.performance.push(
232
- { name: 'encode', duration: encodeEnd - encodeStart }
233
- )
234
-
235
- stream.respond({
236
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
237
- [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
238
- [HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
239
- [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
240
- [HTTP2_HEADER_CACHE_CONTROL]: 'private',
241
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
242
- [HTTP2_HEADER_SERVER]: meta.servername,
243
- [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
244
- [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
245
- [HTTP2_HEADER_ETAG]: meta.etag
246
- })
247
-
248
- // stream.write(encodedData)
249
- stream.end(encodedData)
250
- }
251
-
252
- /**
253
- * @param {ServerHttp2Stream} stream
254
- * @param {string} method
255
- * @param {URL} url
256
- * @param {IncomingHttpHeaders} headers
257
- * @param {Metadata} meta
258
- */
259
- export function sendTrace(stream, method, url, headers, meta) {
260
- const FILTER_KEYS = [ 'authorization', 'cookie' ]
261
- const HTTP_VERSION = new Map([
262
- [ 'h2', 'HTTP/2' ],
263
- [ 'h2c', 'HTTP/2'],
264
- [ 'http/1.1', 'HTTP/1.1']
265
- ])
266
-
267
- const version = HTTP_VERSION.get(stream.session?.alpnProtocol ?? 'h2')
268
-
269
- stream.respond({
270
- [HTTP2_HEADER_CONTENT_TYPE]: MIME_TYPE_MESSAGE_HTTP,
271
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
272
- [HTTP2_HEADER_SERVER]: meta.servername,
273
- [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: meta.origin,
274
- [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance),
275
- })
276
-
277
- const reconstructed = [
278
- `${method} ${url.pathname}${url.search} ${version}`,
279
- Object.entries(headers)
280
- .filter(([ key ]) => !key.startsWith(':'))
281
- .filter(([ key ]) => !FILTER_KEYS.includes(key))
282
- .map(([ key, value ]) => `${key}: ${value}`)
283
- .join('\n'),
284
- '\n'
285
- ]
286
- .join('\n')
287
-
288
- stream.end(reconstructed)
289
- }
290
-
291
- /**
292
- * @param {ServerHttp2Stream} stream
293
- * @param {SSEOptions & Metadata} meta
294
- */
295
- export function sendSSE(stream, meta) {
296
- // stream.setTimeout(0)
297
- // stream.session?.setTimeout(0)
298
- // stream.session?.socket.setTimeout(0)
299
- // stream.session.socket.setNoDelay(true)
300
- // stream.session.socket.setKeepAlive(true)
301
-
302
- // stream.on('close', () => console.log('SSE stream closed'))
303
- // stream.on('aborted', () => console.log('SSE stream aborted'))
304
-
305
- const activeStream = meta.active ?? true
306
- const sendBOM = meta.bom ?? true
307
-
308
- stream.respond({
309
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
310
- [HTTP2_HEADER_CONTENT_TYPE]: SSE_MIME,
311
- [HTTP2_HEADER_STATUS]: activeStream ? HTTP_STATUS_OK : HTTP_STATUS_NO_CONTENT, // SSE_INACTIVE_STATUS_CODE
312
- // [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS]: 'true'
313
- [HTTP2_HEADER_SERVER]: meta.servername
314
- })
315
-
316
- if(!activeStream) {
317
- stream.end()
318
- return
319
- }
320
-
321
- if(sendBOM) {
322
- stream.write(SSE_BOM + ENDING.CRLF)
323
- }
324
- }