@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.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/package.json +18 -0
- package/src/accept-encoding.js +53 -0
- package/src/accept-language.js +46 -0
- package/src/accept-util.js +55 -0
- package/src/accept.js +125 -0
- package/src/body.js +320 -0
- package/src/content-disposition.js +52 -0
- package/src/content-type.js +140 -0
- package/src/forwarded.js +145 -0
- package/src/handle-stream-util.js +262 -0
- package/src/index.js +10 -0
- package/src/multipart.js +149 -0
- package/src/rate-limit.js +84 -0
- package/src/server-timing.js +51 -0
|
@@ -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
|
+
}
|
package/src/forwarded.js
ADDED
|
@@ -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'
|