@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,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
+ // ]))