@johntalton/http-util 1.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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @typedef {Object} Disposition
3
+ * @property {string} disposition
4
+ * @property {Map<string, string>} parameters
5
+ * @property {string} [name]
6
+ * @property {string} [filename]
7
+ */
8
+
9
+ export const DISPOSITION_SEPARATOR = {
10
+ PARAMETER: ';',
11
+ KVP: '='
12
+ }
13
+
14
+ export const DISPOSITION_PARAM_NAME = 'name'
15
+ export const DISPOSITION_PARAM_FILENAME = 'filename'
16
+
17
+ /**
18
+ * @param {string} contentDispositionHeader
19
+ * @returns {Disposition|undefined}
20
+ */
21
+ export function parseContentDisposition(contentDispositionHeader) {
22
+ if(contentDispositionHeader === undefined) { return undefined }
23
+
24
+ const [ disposition, ...parameterSet ] = contentDispositionHeader.trim().split(DISPOSITION_SEPARATOR.PARAMETER).map(entry => entry.trim())
25
+ const parameters = new Map(parameterSet.map(parameter => {
26
+ const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
27
+ return [ key, value ]
28
+ }))
29
+
30
+ const name = parameters.get(DISPOSITION_PARAM_NAME)
31
+ const filename = parameters.get(DISPOSITION_PARAM_FILENAME)
32
+
33
+ return {
34
+ disposition,
35
+ parameters,
36
+ name, filename
37
+ }
38
+ }
39
+
40
+ // console.log(parseContentDisposition())
41
+ // // console.log(parseContentDisposition(null))
42
+ // console.log(parseContentDisposition(''))
43
+ // console.log(parseContentDisposition('form-data'))
44
+ // console.log(parseContentDisposition(' form-data ; name'))
45
+ // console.log(parseContentDisposition('form-data; name="key"'))
46
+
47
+ // console.log(parseContentDisposition('inline'))
48
+ // console.log(parseContentDisposition('attachment'))
49
+ // console.log(parseContentDisposition('attachment; filename="file name.jpg"'))
50
+ // console.log(parseContentDisposition('attachment; filename*=UTF-8\'\'file%20name.jpg'))
51
+ // console.log(parseContentDisposition('attachment; filename*=UTF-8\'\'file%20name.jpg'))
52
+ // console.log(parseContentDisposition('form-data;title*=us-ascii\'en-us\'This%20is%20%2A%2A%2Afun%2A%2A%2A'))
@@ -0,0 +1,140 @@
1
+
2
+ export const MIME_TYPE_JSON = 'application/json'
3
+ export const MIME_TYPE_TEXT = 'text/plain'
4
+ export const MIME_TYPE_EVENT_STREAM = 'text/event-stream'
5
+ export const MIME_TYPE_XML = 'application/xml'
6
+ export const MIME_TYPE_URL_FORM_DATA = 'application/x-www-form-urlencoded'
7
+ export const MIME_TYPE_MULTIPART_FORM_DATA = 'multipart/form-data'
8
+ export const MIME_TYPE_OCTET_STREAM = 'application/octet-stream'
9
+
10
+ export const KNOWN_CONTENT_TYPES = [
11
+ 'application', 'audio', 'image', 'message',
12
+ 'multipart','text', 'video'
13
+ ]
14
+
15
+ export const TYPE_X_TOKEN_PREFIX = 'X-'
16
+
17
+ export const SPECIAL_CHARS = [
18
+ // special
19
+ '(', ')', '<', '>',
20
+ '@', ',', ';', ':',
21
+ '\\', '"', '/', '[',
22
+ ']', '?', '.', '=',
23
+ // space
24
+ ' ', '\u000B', '\u000C',
25
+ // control
26
+ '\n', '\r', '\t'
27
+ ]
28
+
29
+ /**
30
+ * @param {string} c
31
+ */
32
+ export function isWhitespace(c){ return /\s/.test(c) }
33
+
34
+ /**
35
+ * @param {string|undefined} value
36
+ */
37
+ export function hasSpecialChar(value) {
38
+ if(value === undefined) { return false }
39
+ for(const special of SPECIAL_CHARS) {
40
+ if(value.includes(special)) { return true}
41
+ }
42
+
43
+ return false
44
+ }
45
+
46
+ /**
47
+ * @typedef {Object} ContentType
48
+ * @property {string} mimetype
49
+ * @property {string} mimetypeRaw
50
+ * @property {string} type
51
+ * @property {string} subtype
52
+ * @property {string} [charset]
53
+ * @property {Map<string, string>} parameters
54
+ */
55
+
56
+ export const CONTENT_TYPE_SEPARATOR = {
57
+ SUBTYPE: '/',
58
+ PARAMETER: ';',
59
+ KVP: '='
60
+ }
61
+
62
+ export const CHARSET_UTF8 = 'utf8'
63
+ export const CHARSET = 'charset'
64
+ export const PARAMETER_CHARSET_UTF8 = `${CHARSET}${CONTENT_TYPE_SEPARATOR.KVP}${CHARSET_UTF8}`
65
+ export const CONTENT_TYPE_JSON = `${MIME_TYPE_JSON}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
66
+ export const CONTENT_TYPE_TEXT = `${MIME_TYPE_TEXT}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
67
+
68
+ /** @type {ContentType} */
69
+ export const WELL_KNOWN_JSON = {
70
+ mimetype: 'application/json',
71
+ mimetypeRaw: 'application/json',
72
+ type: 'application',
73
+ subtype: 'json',
74
+ charset: 'utf8',
75
+ parameters: new Map()
76
+ }
77
+
78
+ export const WELL_KNOWN_CONTENT_TYPES = new Map([
79
+ [ 'application/json', WELL_KNOWN_JSON ],
80
+ [ 'application/json;charset=utf8', WELL_KNOWN_JSON ]
81
+ ])
82
+
83
+
84
+ /**
85
+ * @param {string|undefined} contentTypeHeader
86
+ * @returns {ContentType|undefined}
87
+ */
88
+ export function parseContentType(contentTypeHeader) {
89
+ if(contentTypeHeader === undefined) { return undefined }
90
+ if(contentTypeHeader === null) { return undefined }
91
+
92
+ const wellKnown = WELL_KNOWN_CONTENT_TYPES.get(contentTypeHeader)
93
+ if(wellKnown !== undefined) { return wellKnown }
94
+
95
+ const [ mimetypeRaw, ...parameterSet ] = contentTypeHeader.split(CONTENT_TYPE_SEPARATOR.PARAMETER)
96
+ if(mimetypeRaw === undefined) { return undefined }
97
+ if(mimetypeRaw === '') { return undefined }
98
+
99
+ const [ typeRaw, subtypeRaw ] = mimetypeRaw
100
+ .split(CONTENT_TYPE_SEPARATOR.SUBTYPE)
101
+ .map(t => t.toLowerCase())
102
+
103
+ if(typeRaw === undefined) { return undefined }
104
+ if(typeRaw === '') { return undefined }
105
+ if(hasSpecialChar(typeRaw)) { return undefined }
106
+ if(subtypeRaw === undefined) { return undefined }
107
+ if(subtypeRaw === '') { return undefined }
108
+ if(hasSpecialChar(subtypeRaw)) { return undefined }
109
+
110
+ const type = typeRaw.trim()
111
+ const subtype = subtypeRaw.trim()
112
+
113
+ const parameters = new Map()
114
+
115
+ parameterSet
116
+ .forEach(parameter => {
117
+ const [ key, value ] = parameter.split(CONTENT_TYPE_SEPARATOR.KVP)
118
+ if(key === undefined || key === '') { return }
119
+ if(value === undefined || value === '') { return }
120
+ if(hasSpecialChar(key)) { return }
121
+
122
+ const actualKey = key?.trim().toLowerCase()
123
+
124
+ const quoted = (value.charAt(0) === '"' && value.charAt(value.length - 1) === '"')
125
+ const actualValue = quoted ? value.substring(1, value.length - 1) : value
126
+
127
+ if(!parameters.has(actualKey)) {
128
+ parameters.set(actualKey, actualValue)
129
+ }
130
+ })
131
+
132
+ const charset = parameters.get(CHARSET)
133
+
134
+ return {
135
+ mimetype: `${type}${CONTENT_TYPE_SEPARATOR.SUBTYPE}${subtype}`,
136
+ mimetypeRaw, type, subtype,
137
+ charset,
138
+ parameters
139
+ }
140
+ }
@@ -0,0 +1,145 @@
1
+ export const FORWARDED_KEY_BY = 'by'
2
+ export const FORWARDED_KEY_FOR = 'for'
3
+ export const FORWARDED_KEY_HOST = 'host'
4
+ export const FORWARDED_KEY_PROTO = 'proto'
5
+
6
+ export const KNOWN_FORWARDED_KEYS = [
7
+ FORWARDED_KEY_BY,
8
+ FORWARDED_KEY_FOR,
9
+ FORWARDED_KEY_HOST,
10
+ FORWARDED_KEY_PROTO
11
+ ]
12
+
13
+ export const SKIP_ANY = '*'
14
+
15
+ export const FORWARDED_SEPARATOR = {
16
+ ITEM: ',',
17
+ ELEMENT: ';',
18
+ KVP: '='
19
+ }
20
+
21
+ export class Forwarded {
22
+ /**
23
+ * @param {string|undefined} header
24
+ * @param {Array<string>} acceptedKeys
25
+ * @returns {Array<Map<string, string>>}
26
+ */
27
+ static parse(header, acceptedKeys = KNOWN_FORWARDED_KEYS) {
28
+ if(typeof header !== 'string') { return [] }
29
+
30
+ return header
31
+ .trim()
32
+ .split(FORWARDED_SEPARATOR.ITEM)
33
+ .map(single => new Map(single
34
+ .trim()
35
+ .split(FORWARDED_SEPARATOR.ELEMENT)
36
+ .map(kvp => {
37
+ const [ rawKey, rawValue ] = kvp.trim().split(FORWARDED_SEPARATOR.KVP)
38
+
39
+ const key = rawKey?.trim()?.toLowerCase()
40
+ if (key === undefined || !acceptedKeys.includes(key)) { return undefined }
41
+
42
+ const value = rawValue?.trim()
43
+ if(value === undefined) { return undefined }
44
+ if(value.length <= 0) { return undefined }
45
+
46
+ /** @type {[string, string]} */
47
+ const result = [ key, value ]
48
+ return result
49
+ })
50
+ .filter(item => item !== undefined))
51
+ )
52
+ .filter(m => m.size !== 0)
53
+ }
54
+
55
+ /**
56
+ * @param {Array<Map<string, string>>} forwardedList
57
+ * @param {Array<string>} skipList list of for values starting with right-most to skip in forwarded list
58
+ * @returns {Map<string, string>|undefined}
59
+ */
60
+ static selectRightMost(forwardedList, skipList = []) {
61
+ const iter = skipList[Symbol.iterator]()
62
+
63
+ for(const forwarded of forwardedList.toReversed()) {
64
+ const forValue = forwarded.get(FORWARDED_KEY_FOR)
65
+ const { done, value } = iter.next()
66
+ if(done) { return forwarded }
67
+ if(value !== SKIP_ANY && value !== forValue) { return undefined }
68
+ }
69
+
70
+ return undefined
71
+ }
72
+ }
73
+
74
+
75
+ /*
76
+ const examples = [
77
+ { f: [], s: [], ef: undefined },
78
+
79
+ { f: [], s: [ '1.1.1.1' ] , ef: undefined },
80
+ { f: [], s: [ '*' ] , ef: undefined },
81
+
82
+ { f: [ { for: '1.1.1.1' } ], s: [], ef: '1.1.1.1' },
83
+ { f: [ { for: '1.1.1.1' } ], s: [ '*' ], ef: undefined },
84
+ { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' } ], s: [], ef: '2.2.2.2' },
85
+ { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' } ], s: [ '2.2.2.2' ], ef: '1.1.1.1' },
86
+ { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3', '2.2.2.2' ], ef: '1.1.1.1' },
87
+ { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3', '*' ], ef: '1.1.1.1' },
88
+ { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*', '*' ], ef: '1.1.1.1' },
89
+ { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*', '*', '*' ], ef: undefined },
90
+
91
+ { f: [ { for: '1.1.1.1' } ], s: [ '*', '*' ], ef: undefined },
92
+
93
+ { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '3.3.3.3'], ef: '2.2.2.2' },
94
+ { f: [ { for: '1.1.1.1' }, { for: '2.2.2.2' }, { for: '3.3.3.3' } ], s: [ '*'], ef: '2.2.2.2' },
95
+ ]
96
+
97
+ for(const { f, s, ef } of examples) {
98
+ const result = Forwarded.selectRightMost(f.map(i => new Map(Object.entries(i))), s)
99
+ const resultFor = result?.get('for')
100
+ if(resultFor !== ef) {
101
+ console.log(`mismatch ${ef} !== ${resultFor}`)
102
+ }
103
+ }
104
+ */
105
+
106
+
107
+ /*
108
+ const examples = [
109
+ null,
110
+ undefined,
111
+ 42,
112
+ '',
113
+ ' ',
114
+ 'some value here',
115
+ '======;;;',
116
+ ',=,=,=;;;==,,,for',
117
+ 'a=2, b=3, by=, =10',
118
+ 'by=🚀',
119
+ '🔑="a key"',
120
+
121
+ 'for="_gazonk"',
122
+ 'for="_mdn"',
123
+ 'For="[2001:db8:cafe::17]:4711"',
124
+ 'for=192.0.2.60;proto=http;by=203.0.113.43',
125
+ 'for=192.0.2.43, for=198.51.100.17',
126
+ 'for=192.0.2.43',
127
+ 'for=192.0.2.43, for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com"',
128
+ 'for=192.0.2.43, for="[2001:db8:cafe::17]"',
129
+ 'for=192.0.2.43,for="[2001:db8:cafe::17]",for=unknown',
130
+ 'for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown',
131
+ 'for=_hidden, for=_SEVKISEK',
132
+ 'for=192.0.2.43, for="[2001:db8:cafe::17]", for=unknown',
133
+
134
+ ' for = 1.1.1.1 ,for= 2.2.2.2 ',
135
+ ' fro=not_real, for=[::1]',
136
+ 'FOr=192.0.2.43:47011,for="[2001:db8:cafe::17]:47011"',
137
+ ' for=12.34.56.78, for=23.45.67.89;secret=egah2CGj55fSJFs, for=10.1.2.3'
138
+ ]
139
+
140
+ for (const example of examples) {
141
+ console.log('================================')
142
+ console.log(example)
143
+ console.log(Forwarded.parse(example, [ ...KNOWN_FORWARDED_KEYS, 'secret', '🔑']))
144
+ }
145
+ */
@@ -0,0 +1,262 @@
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
+ }
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from './accept-encoding.js'
2
+ export * from './accept-language.js'
3
+ export * from './accept-util.js'
4
+ export * from './accept.js'
5
+ export * from './content-disposition.js'
6
+ export * from './content-type.js'
7
+ export * from './forwarded.js'
8
+ export * from './multipart.js'
9
+ export * from './rate-limit.js'
10
+ export * from './server-timing.js'