@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
package/src/multipart.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { parseContentDisposition } from './content-disposition.js'
|
|
2
|
+
import { parseContentType } from './content-type.js'
|
|
3
|
+
|
|
4
|
+
export const DISPOSITION_FORM_DATA = 'form-data'
|
|
5
|
+
|
|
6
|
+
export const BOUNDARY_MARK = '--'
|
|
7
|
+
export const MULTIPART_SEPARATOR = '\r\n'
|
|
8
|
+
|
|
9
|
+
export const HEADER_SEPARATOR = ':'
|
|
10
|
+
|
|
11
|
+
export const EMPTY = ''
|
|
12
|
+
|
|
13
|
+
export const MULTIPART_HEADER = {
|
|
14
|
+
CONTENT_DISPOSITION: 'content-disposition',
|
|
15
|
+
CONTENT_TYPE: 'content-type'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const MULTIPART_STATE = {
|
|
19
|
+
BEGIN: 'begin',
|
|
20
|
+
HEADERS: 'headers',
|
|
21
|
+
VALUE: 'value',
|
|
22
|
+
BEGIN_OR_END: 'beginOrEnd'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Multipart {
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} text
|
|
28
|
+
* @param {string} boundary
|
|
29
|
+
* @param {string} [charset='utf8']
|
|
30
|
+
*/
|
|
31
|
+
static parse(text, boundary, charset = 'utf8') {
|
|
32
|
+
// console.log({ boundary, text })
|
|
33
|
+
const formData = new FormData()
|
|
34
|
+
|
|
35
|
+
if(text === '') {
|
|
36
|
+
// empty body
|
|
37
|
+
return formData
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const lines = text.split(MULTIPART_SEPARATOR)
|
|
41
|
+
|
|
42
|
+
if(lines.length === 0) {
|
|
43
|
+
// missing body?
|
|
44
|
+
return formData
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const boundaryBegin = `${BOUNDARY_MARK}${boundary}`
|
|
48
|
+
const boundaryEnd = `${BOUNDARY_MARK}${boundary}${BOUNDARY_MARK}`
|
|
49
|
+
|
|
50
|
+
let partName = undefined
|
|
51
|
+
let state = MULTIPART_STATE.BEGIN
|
|
52
|
+
|
|
53
|
+
for(const line of lines) {
|
|
54
|
+
// console.log('line', line)
|
|
55
|
+
|
|
56
|
+
if(state === MULTIPART_STATE.BEGIN) {
|
|
57
|
+
// expect boundary
|
|
58
|
+
if(line === boundaryEnd) {
|
|
59
|
+
// empty set
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if(line !== boundaryBegin) {
|
|
64
|
+
throw new Error('missing beginning boundary')
|
|
65
|
+
}
|
|
66
|
+
state = MULTIPART_STATE.HEADERS
|
|
67
|
+
}
|
|
68
|
+
else if(state === MULTIPART_STATE.HEADERS) {
|
|
69
|
+
if(line === EMPTY) { state = MULTIPART_STATE.VALUE }
|
|
70
|
+
else {
|
|
71
|
+
const [ rawName, value ] = line.split(HEADER_SEPARATOR)
|
|
72
|
+
const name = rawName.toLowerCase()
|
|
73
|
+
// console.log('header', name, value)
|
|
74
|
+
if(name === MULTIPART_HEADER.CONTENT_TYPE) {
|
|
75
|
+
const contentType = parseContentType(value)
|
|
76
|
+
// console.log({ contentType })
|
|
77
|
+
}
|
|
78
|
+
else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
|
|
79
|
+
const disposition = parseContentDisposition(value)
|
|
80
|
+
if(disposition?.disposition !== DISPOSITION_FORM_DATA) {
|
|
81
|
+
throw new Error('disposition not form-data')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// todo: are names always quoted?
|
|
85
|
+
partName = disposition.name?.slice(1, -1)
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// unsupported part header - ignore
|
|
89
|
+
console.log('unsupported part header', name)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if(state === MULTIPART_STATE.VALUE) {
|
|
94
|
+
// console.log('value', line)
|
|
95
|
+
if(partName === undefined) { throw new Error('unnamed part') }
|
|
96
|
+
|
|
97
|
+
formData.append(partName, line)
|
|
98
|
+
partName = undefined
|
|
99
|
+
|
|
100
|
+
state = MULTIPART_STATE.BEGIN_OR_END
|
|
101
|
+
}
|
|
102
|
+
else if(state === MULTIPART_STATE.BEGIN_OR_END) {
|
|
103
|
+
if(line === boundaryEnd) { break }
|
|
104
|
+
if(line !== boundaryBegin) {
|
|
105
|
+
throw new Error('missing boundary or end')
|
|
106
|
+
}
|
|
107
|
+
state = MULTIPART_STATE.HEADERS
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw new Error('unknown state')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return formData
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// const test = '--X-INSOMNIA-BOUNDARY\r\n' +
|
|
122
|
+
// 'Content-Disposition: form-data; name="u"\r\n' +
|
|
123
|
+
// '\r\n' +
|
|
124
|
+
// 'alice\r\n' +
|
|
125
|
+
// '--X-INSOMNIA-BOUNDARY\r\n' +
|
|
126
|
+
// 'Content-Disposition: form-data; name="user"\r\n' +
|
|
127
|
+
// '\r\n' +
|
|
128
|
+
// 'jeff\r\n' +
|
|
129
|
+
// '--X-INSOMNIA-BOUNDARY--\r\n'
|
|
130
|
+
// const result = Multipart.parse(test, 'X-INSOMNIA-BOUNDARY')
|
|
131
|
+
// console.log(result)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// const test = [
|
|
135
|
+
// '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
|
|
136
|
+
// 'Content-Disposition: form-data; '
|
|
137
|
+
// + 'name="upload_file_0"; filename="テスト.dat"',
|
|
138
|
+
// 'Content-Type: application/octet-stream',
|
|
139
|
+
// '',
|
|
140
|
+
// 'A'.repeat(1023),
|
|
141
|
+
// '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
|
|
142
|
+
// ].join('\r\n')
|
|
143
|
+
// const result = Multipart.parse(test, '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k')
|
|
144
|
+
// console.log(result)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
// const test = '--X-INSOMNIA-BOUNDARY--\r\n'
|
|
148
|
+
// const result = Multipart.parse(test, 'X-INSOMNIA-BOUNDARY')
|
|
149
|
+
// console.log(result)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const HTTP_HEADER_RATE_LIMIT = 'RateLimit'
|
|
2
|
+
export const HTTP_HEADER_RATE_LIMIT_POLICY = 'RateLimit-Policy'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} RateLimitInfo
|
|
6
|
+
* @property {string} name
|
|
7
|
+
* @property {number} remaining
|
|
8
|
+
* @property {number} resetSeconds
|
|
9
|
+
* @property {string} partitionKey
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} RateLimitPolicyInfo
|
|
14
|
+
* @property {string} name
|
|
15
|
+
* @property {number} quota
|
|
16
|
+
* @property {number} size
|
|
17
|
+
* @property {number} quotaUnits
|
|
18
|
+
* @property {number} windowSeconds
|
|
19
|
+
* @property {string} [partitionKey]
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const LIMIT_PARAMETERS = {
|
|
23
|
+
REMAINING_QUOTA: 'r',
|
|
24
|
+
TIME_TILL_RESET_SECONDS: 't',
|
|
25
|
+
PARTITION_KEY: 'pk'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const POLICY_PARAMETER = {
|
|
29
|
+
QUOTA: 'q',
|
|
30
|
+
QUOTA_UNITS: 'qu',
|
|
31
|
+
WINDOW_SECONDS: 'w',
|
|
32
|
+
PARTITION_KEY: 'pk'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const QUOTA_UNIT = {
|
|
36
|
+
REQUEST: 'request',
|
|
37
|
+
BYTES: 'content-bytes',
|
|
38
|
+
CONCURRENT: 'concurrent-requests'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class RateLimit {
|
|
42
|
+
/**
|
|
43
|
+
* @param {RateLimitInfo} limitInfo
|
|
44
|
+
*/
|
|
45
|
+
static from(limitInfo) {
|
|
46
|
+
const { name, remaining, resetSeconds, partitionKey } = limitInfo
|
|
47
|
+
|
|
48
|
+
if(name === undefined || remaining === undefined) { return undefined }
|
|
49
|
+
|
|
50
|
+
const rs = resetSeconds ? `;${LIMIT_PARAMETERS.TIME_TILL_RESET_SECONDS}=${resetSeconds}` : ''
|
|
51
|
+
const pk = partitionKey ? `'${LIMIT_PARAMETERS.PARTITION_KEY}=${partitionKey}` : ''
|
|
52
|
+
return `"${name}";${LIMIT_PARAMETERS.REMAINING_QUOTA}=${remaining}${rs}${pk}`
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class RateLimitPolicy {
|
|
57
|
+
/**
|
|
58
|
+
* @param {...RateLimitPolicyInfo} policies
|
|
59
|
+
*/
|
|
60
|
+
static from(...policies) {
|
|
61
|
+
if(policies === undefined) { return undefined }
|
|
62
|
+
|
|
63
|
+
return policies
|
|
64
|
+
.filter(policy => policy.name !== undefined && policy.quota !== undefined)
|
|
65
|
+
.map(policy => {
|
|
66
|
+
const {
|
|
67
|
+
name,
|
|
68
|
+
quota,
|
|
69
|
+
quotaUnits,
|
|
70
|
+
windowSeconds,
|
|
71
|
+
partitionKey
|
|
72
|
+
} = policy
|
|
73
|
+
|
|
74
|
+
const q = quota ? `${POLICY_PARAMETER.QUOTA}=${quota}` : undefined
|
|
75
|
+
const qu = quotaUnits ? `${POLICY_PARAMETER.QUOTA_UNITS}="${quotaUnits}"` : undefined
|
|
76
|
+
const ws = windowSeconds ? `${POLICY_PARAMETER.WINDOW_SECONDS}=${windowSeconds}` : undefined
|
|
77
|
+
const pk = partitionKey ? `${POLICY_PARAMETER.PARTITION_KEY}=${partitionKey}` : undefined
|
|
78
|
+
return [ `"${name}"`, q, qu, ws, pk ]
|
|
79
|
+
.filter(item => item !== undefined)
|
|
80
|
+
.join(';')
|
|
81
|
+
})
|
|
82
|
+
.join(',')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const SERVER_TIMING_KEY_DURATION = 'dur' // common in milliseconds
|
|
2
|
+
export const SERVER_TIMING_KEY_DESCRIPTION = 'desc'
|
|
3
|
+
|
|
4
|
+
export const HTTP_HEADER_SERVER_TIMING = 'Server-Timing'
|
|
5
|
+
export const HTTP_HEADER_TIMING_ALLOW_ORIGIN = 'Timing-Allow-Origin'
|
|
6
|
+
|
|
7
|
+
export const SERVER_TIMING_SEPARATOR = {
|
|
8
|
+
METRIC: ',',
|
|
9
|
+
PARAMETER: ';',
|
|
10
|
+
KVP: '='
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} TimingsInfo
|
|
15
|
+
* @property {string} name
|
|
16
|
+
* @property {number|undefined} duration
|
|
17
|
+
* @property {string|undefined} [description]
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class ServerTiming {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Array<TimingsInfo>} timings
|
|
23
|
+
*/
|
|
24
|
+
static encode(timings) {
|
|
25
|
+
if(timings === undefined) { return undefined }
|
|
26
|
+
if(timings.length <= 0) { return undefined }
|
|
27
|
+
|
|
28
|
+
return timings
|
|
29
|
+
.map(({ name, duration, description }) => [
|
|
30
|
+
`${name}`,
|
|
31
|
+
description !== undefined ? `${SERVER_TIMING_KEY_DESCRIPTION}${SERVER_TIMING_SEPARATOR.KVP}"${description}"` : undefined,
|
|
32
|
+
duration !== undefined ? `${SERVER_TIMING_KEY_DURATION}${SERVER_TIMING_SEPARATOR.KVP}${Math.trunc(duration * 10) / 10}` : undefined
|
|
33
|
+
]
|
|
34
|
+
.filter(item => item !== undefined)
|
|
35
|
+
.join(SERVER_TIMING_SEPARATOR.PARAMETER))
|
|
36
|
+
.join(SERVER_TIMING_SEPARATOR.METRIC)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// console.log(ServerTiming.encode([{ name: 'missedCache' }]))
|
|
42
|
+
// console.log(ServerTiming.encode([{ name: 'cpu', duration: 2.4 }]))
|
|
43
|
+
|
|
44
|
+
// // cache;desc="Cache Read";dur=23.2
|
|
45
|
+
// console.log(ServerTiming.encode([{ name: 'cache', duration: 23.2, description: "Cache Read" }]))
|
|
46
|
+
|
|
47
|
+
// // db;dur=53, app;dur=47.2
|
|
48
|
+
// console.log(ServerTiming.encode([
|
|
49
|
+
// { name: 'db', duration: 54 },
|
|
50
|
+
// { name: 'app', duration: 47.2 }
|
|
51
|
+
// ]))
|