@johntalton/http-util 1.0.1 → 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": "1.0.1",
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"
@@ -6,6 +6,7 @@ export const MIME_TYPE_XML = 'application/xml'
6
6
  export const MIME_TYPE_URL_FORM_DATA = 'application/x-www-form-urlencoded'
7
7
  export const MIME_TYPE_MULTIPART_FORM_DATA = 'multipart/form-data'
8
8
  export const MIME_TYPE_OCTET_STREAM = 'application/octet-stream'
9
+ export const MIME_TYPE_MESSAGE_HTTP = 'message/http'
9
10
 
10
11
  export const KNOWN_CONTENT_TYPES = [
11
12
  'application', 'audio', 'image', 'message',
@@ -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,262 +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 { CHARSET_UTF8, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT } from './content-type.js'
12
- import { ServerTiming, HTTP_HEADER_SERVER_TIMING, HTTP_HEADER_TIMING_ALLOW_ORIGIN } from './server-timing.js'
13
- import { HTTP_HEADER_RATE_LIMIT, HTTP_HEADER_RATE_LIMIT_POLICY, RateLimit, RateLimitPolicy } from './rate-limit.js'
14
-
15
- const {
16
- HTTP2_HEADER_STATUS,
17
- HTTP2_HEADER_CONTENT_TYPE,
18
- HTTP2_HEADER_CONTENT_ENCODING,
19
- HTTP2_HEADER_VARY,
20
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
21
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS,
22
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
23
- HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
24
- HTTP2_HEADER_SERVER,
25
- HTTP2_HEADER_RETRY_AFTER,
26
- HTTP2_HEADER_CACHE_CONTROL
27
- } = http2.constants
28
-
29
- const {
30
- HTTP_STATUS_OK,
31
- HTTP_STATUS_NOT_FOUND,
32
- HTTP_STATUS_UNAUTHORIZED,
33
- HTTP_STATUS_NO_CONTENT,
34
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
35
- HTTP_STATUS_TOO_MANY_REQUESTS
36
- } = http2.constants
37
-
38
- export const HTTP_HEADER_ORIGIN = 'origin'
39
- export const HTTP_HEADER_USER_AGENT = 'user-agent'
40
- export const HTTP_HEADER_FORWARDED = 'forwarded'
41
- export const HTTP_HEADER_SEC_CH_UA = 'sec-ch-ua'
42
- export const HTTP_HEADER_SEC_CH_PLATFORM = 'sec-ch-ua-platform'
43
- export const HTTP_HEADER_SEC_CH_MOBILE = 'sec-ch-ua-mobile'
44
- export const HTTP_HEADER_SEC_FETCH_SITE = 'sec-fetch-site'
45
- export const HTTP_HEADER_SEC_FETCH_MODE = 'sec-fetch-mode'
46
- export const HTTP_HEADER_SEC_FETCH_DEST = 'sec-fetch-dest'
47
-
48
- export const DEFAULT_METHODS = [ 'HEAD', 'GET', 'POST', 'PATCH', 'DELETE' ]
49
-
50
- export const HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE = 'access-control-max-age'
51
- export const PREFLIGHT_AGE_SECONDS = '500'
52
-
53
- /**
54
- * @import { ServerHttp2Stream } from 'node:http2'
55
- */
56
-
57
- /**
58
- * @import { TimingsInfo } from './server-timing.js'
59
- */
60
-
61
- /**
62
- * @typedef {Object} Metadata
63
- * @property {Array<TimingsInfo>} performance
64
- * @property {string|undefined} servername
65
- * @property {string|undefined} origin
66
- */
67
-
68
- /**
69
- * @typedef {Object} SSEOptions
70
- * @property {boolean} [active]
71
- * @property {boolean} [bom]
72
- */
73
-
74
- /**
75
- * @param {ServerHttp2Stream} stream
76
- * @param {string} message
77
- * @param {Metadata} meta
78
- */
79
- export function sendError(stream, message, meta) {
80
- console.error('500', message)
81
-
82
- if(stream === undefined) { return }
83
- if(stream.closed) { return }
84
-
85
- if(!stream.headersSent) {
86
- stream.respond({
87
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
88
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_INTERNAL_SERVER_ERROR,
89
- [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_TEXT,
90
- [HTTP2_HEADER_SERVER]: meta.servername
91
- })
92
- }
93
-
94
- // protect against HEAD calls
95
- if(stream.writable) {
96
- if(message !== undefined) { stream.write(message) }
97
- }
98
-
99
- stream.end()
100
- if(!stream.closed) { stream.close() }
101
- }
102
-
103
- /**
104
- * @param {ServerHttp2Stream} stream
105
- * @param {string|undefined} allowedOrigin
106
- * @param {Array<string>} methods
107
- * @param {Metadata} meta
108
- */
109
- export function sendPreflight(stream, allowedOrigin, methods, meta) {
110
- stream.respond({
111
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
112
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: allowedOrigin,
113
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS]: methods.join(','),
114
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS]: ['Authorization', HTTP2_HEADER_CONTENT_TYPE].join(','),
115
- [HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE]: PREFLIGHT_AGE_SECONDS,
116
- [HTTP2_HEADER_SERVER]: meta.servername
117
- })
118
- stream.end()
119
- }
120
-
121
- /**
122
- * @param {ServerHttp2Stream} stream
123
- * @param {Metadata} meta
124
- */
125
- export function sendUnauthorized(stream, meta) {
126
- console.log('Unauthorized')
127
-
128
- stream.respond({
129
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
130
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_UNAUTHORIZED,
131
- [HTTP2_HEADER_SERVER]: meta.servername
132
- })
133
- stream.end()
134
- }
135
-
136
- /**
137
- * @param {ServerHttp2Stream} stream
138
- * @param {string} message
139
- * @param {Metadata} meta
140
- */
141
- export function sendNotFound(stream, message, meta) {
142
- console.log('404', message)
143
- stream.respond({
144
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
145
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_NOT_FOUND,
146
- [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_TEXT,
147
- [HTTP2_HEADER_SERVER]: meta.servername
148
- })
149
-
150
- if(message !== undefined) { stream.write(message) }
151
- stream.end()
152
- }
153
-
154
- /**
155
- * @param {ServerHttp2Stream} stream
156
- * @param {*} limitInfo
157
- * @param {Array<any>} policies
158
- * @param {Metadata} meta
159
- */
160
- export function sendTooManyRequests(stream, limitInfo, policies, meta) {
161
- stream.respond({
162
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: meta.origin,
163
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_TOO_MANY_REQUESTS,
164
- [HTTP2_HEADER_SERVER]: meta.servername,
165
-
166
- [HTTP2_HEADER_RETRY_AFTER]: limitInfo.retryAfterS,
167
- [HTTP_HEADER_RATE_LIMIT]: RateLimit.from(limitInfo),
168
- [HTTP_HEADER_RATE_LIMIT_POLICY]: RateLimitPolicy.from(...policies)
169
- })
170
-
171
- stream.write(`Retry After ${limitInfo.retryAfterS} Seconds`)
172
- stream.end()
173
- }
174
-
175
- /**
176
- * @typedef { (data: string, charset: BufferEncoding) => Buffer } EncoderFun
177
- */
178
-
179
- /** @type {Map<string, EncoderFun>} */
180
- export const ENCODER_MAP = new Map([
181
- [ 'br', (data, charset) => brotliCompressSync(Buffer.from(data, charset)) ],
182
- [ 'gzip', (data, charset) => gzipSync(Buffer.from(data, charset)) ],
183
- [ 'deflate', (data, charset) => deflateSync(Buffer.from(data, charset)) ],
184
- [ 'zstd', (data, charset) => zstdCompressSync(Buffer.from(data, charset)) ]
185
- ])
186
-
187
- /**
188
- * @param {ServerHttp2Stream} stream
189
- * @param {Object} obj
190
- * @param {string|undefined} encoding
191
- * @param {string|undefined} allowedOrigin
192
- * @param {Metadata} meta
193
- */
194
- export function sendJSON_Encoded(stream, obj, encoding, allowedOrigin, meta) {
195
- if(stream.closed) { return }
196
-
197
- const json = JSON.stringify(obj)
198
-
199
- const useIdentity = encoding === 'identity'
200
- const encoder = encoding !== undefined ? ENCODER_MAP.get(encoding) : undefined
201
- const hasEncoder = encoder !== undefined
202
- const actualEncoding = hasEncoder ? encoding : undefined
203
-
204
- const encodeStart = performance.now()
205
- const encodedData = hasEncoder && !useIdentity ? encoder(json, CHARSET_UTF8) : json
206
- const encodeEnd = performance.now()
207
-
208
- meta.performance.push(
209
- { name: 'encode', duration: encodeEnd - encodeStart }
210
- )
211
-
212
- stream.respond({
213
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: allowedOrigin,
214
- [HTTP2_HEADER_CONTENT_TYPE]: CONTENT_TYPE_JSON,
215
- [HTTP2_HEADER_CONTENT_ENCODING]: actualEncoding,
216
- [HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
217
- [HTTP2_HEADER_CACHE_CONTROL]: 'private',
218
- [HTTP2_HEADER_STATUS]: HTTP_STATUS_OK,
219
- [HTTP2_HEADER_SERVER]: meta.servername,
220
- [HTTP_HEADER_TIMING_ALLOW_ORIGIN]: allowedOrigin,
221
- [HTTP_HEADER_SERVER_TIMING]: ServerTiming.encode(meta.performance)
222
- })
223
-
224
- // stream.write(encodedData)
225
- stream.end(encodedData)
226
- }
227
-
228
- /**
229
- * @param {ServerHttp2Stream} stream
230
- * @param {string|undefined} allowedOrigin
231
- * @param {SSEOptions & Metadata} meta
232
- */
233
- export function sendSSE(stream, allowedOrigin, meta) {
234
- // stream.setTimeout(0)
235
- // stream.session?.setTimeout(0)
236
- // stream.session?.socket.setTimeout(0)
237
- // stream.session.socket.setNoDelay(true)
238
- // stream.session.socket.setKeepAlive(true)
239
-
240
- // stream.on('close', () => console.log('SSE stream closed'))
241
- // stream.on('aborted', () => console.log('SSE stream aborted'))
242
-
243
- const activeStream = meta.active ?? true
244
- const sendBOM = meta.bom ?? true
245
-
246
- stream.respond({
247
- [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN]: allowedOrigin,
248
- [HTTP2_HEADER_CONTENT_TYPE]: SSE_MIME,
249
- [HTTP2_HEADER_STATUS]: activeStream ? HTTP_STATUS_OK : HTTP_STATUS_NO_CONTENT, // SSE_INACTIVE_STATUS_CODE
250
- // [HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS]: 'true'
251
- [HTTP2_HEADER_SERVER]: meta.servername
252
- })
253
-
254
- if(!activeStream) {
255
- stream.end()
256
- return
257
- }
258
-
259
- if(sendBOM) {
260
- stream.write(SSE_BOM + ENDING.CRLF)
261
- }
262
- }