@johntalton/http-util 2.0.0 → 2.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.
@@ -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
- }