@johntalton/http-util 5.1.6 → 6.1.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/README.md +54 -7
- package/package.json +26 -6
- package/src/body.js +8 -8
- package/src/{response/defs.js → defs.js} +35 -1
- package/src/{accept-encoding.js → headers/accept-encoding.js} +11 -14
- package/src/{accept-language.js → headers/accept-language.js} +14 -9
- package/src/headers/accept.js +86 -0
- package/src/{cache-control.js → headers/cache-control.js} +0 -6
- package/src/{clear-site-data.js → headers/clear-site-data.js} +4 -10
- package/src/headers/client-hints.js +88 -0
- package/src/{conditional.js → headers/conditional.js} +190 -117
- package/src/headers/content-disposition.js +44 -0
- package/src/{content-range.js → headers/content-range.js} +1 -18
- package/src/headers/content-type.js +101 -0
- package/src/{forwarded.js → headers/forwarded.js} +8 -56
- package/src/{index.js → headers/index.js} +4 -2
- package/src/headers/link.js +34 -0
- package/src/{multipart.js → headers/multipart.js} +22 -13
- package/src/{preference.js → headers/preference.js} +3 -58
- package/src/{range.js → headers/range.js} +4 -32
- package/src/{rate-limit.js → headers/rate-limit.js} +6 -1
- package/src/{server-timing.js → headers/server-timing.js} +3 -16
- package/src/headers/strict-transport-security.js +39 -0
- package/src/{accept-util.js → headers/util/accept-util.js} +8 -14
- package/src/headers/util/index.js +7 -0
- package/src/headers/util/kvp.js +79 -0
- package/src/headers/util/mime.js +77 -0
- package/src/headers/util/whitespace.js +8 -0
- package/src/{www-authenticate.js → headers/www-authenticate.js} +1 -1
- package/src/response/{accepted.js → 2xx/accepted.js} +2 -2
- package/src/response/2xx/bytes.js +62 -0
- package/src/response/2xx/created.js +49 -0
- package/src/response/2xx/json.js +60 -0
- package/src/response/2xx/no-content.js +45 -0
- package/src/response/2xx/partial-content.js +101 -0
- package/src/response/{preflight.js → 2xx/preflight.js} +29 -10
- package/src/response/{sse.js → 2xx/sse.js} +2 -2
- package/src/response/{trace.js → 2xx/trace.js} +3 -3
- package/src/response/3xx/found.js +23 -0
- package/src/response/{moved-permanently.js → 3xx/moved-permanently.js} +2 -2
- package/src/response/{multiple-choices.js → 3xx/multiple-choices.js} +2 -3
- package/src/response/3xx/not-modified.js +59 -0
- package/src/response/{permanent-redirect.js → 3xx/permanent-redirect.js} +2 -2
- package/src/response/{see-other.js → 3xx/see-other.js} +2 -2
- package/src/response/{temporary-redirect.js → 3xx/temporary-redirect.js} +2 -2
- package/src/response/4xx/bad-request.js +19 -0
- package/src/response/{conflict.js → 4xx/conflict.js} +2 -2
- package/src/response/{content-too-large.js → 4xx/content-too-large.js} +2 -2
- package/src/response/{forbidden.js → 4xx/forbidden.js} +3 -2
- package/src/response/{gone.js → 4xx/gone.js} +2 -2
- package/src/response/{im-a-teapot.js → 4xx/im-a-teapot.js} +2 -2
- package/src/response/{not-acceptable.js → 4xx/not-acceptable.js} +14 -3
- package/src/response/4xx/not-allowed.js +34 -0
- package/src/response/{not-found.js → 4xx/not-found.js} +3 -3
- package/src/response/4xx/payment-required.js +17 -0
- package/src/response/4xx/precondition-failed.js +45 -0
- package/src/response/{range-not-satisfiable.js → 4xx/range-not-satisfiable.js} +15 -4
- package/src/response/{timeout.js → 4xx/timeout.js} +2 -2
- package/src/response/{too-many-requests.js → 4xx/too-many-requests.js} +22 -5
- package/src/response/{unauthorized.js → 4xx/unauthorized.js} +5 -5
- package/src/response/{unprocessable.js → 4xx/unprocessable.js} +2 -2
- package/src/response/{unsupported-media.js → 4xx/unsupported-media.js} +21 -4
- package/src/response/{error.js → 5xx/error.js} +3 -3
- package/src/response/{insufficient-storage.js → 5xx/insufficient-storage.js} +2 -2
- package/src/response/{not-implemented.js → 5xx/not-implemented.js} +4 -4
- package/src/response/{unavailable.js → 5xx/unavailable.js} +16 -4
- package/src/response/header-util.js +2 -2
- package/src/response/index.js +39 -35
- package/src/response/response.js +40 -34
- package/src/response/send-util.js +32 -21
- package/src/accept.js +0 -122
- package/src/content-disposition.js +0 -57
- package/src/content-type.js +0 -148
- package/src/link.js +0 -35
- package/src/response/bytes.js +0 -27
- package/src/response/created.js +0 -28
- package/src/response/json.js +0 -28
- package/src/response/no-content.js +0 -25
- package/src/response/not-allowed.js +0 -23
- package/src/response/not-modified.js +0 -35
- package/src/response/partial-content.js +0 -71
- package/src/response/precondition-failed.js +0 -16
- /package/src/{fetch-metadata.js → headers/fetch-metadata.js} +0 -0
- /package/src/{quote.js → headers/util/quote.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import http2 from 'node:http2'
|
|
2
|
-
import { Readable } from 'node:stream'
|
|
2
|
+
import { pipeline, Readable } from 'node:stream'
|
|
3
3
|
import { ReadableStream } from 'node:stream/web'
|
|
4
4
|
import {
|
|
5
5
|
brotliCompressSync,
|
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
zstdCompressSync
|
|
9
9
|
} from 'node:zlib'
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
11
|
+
import { HTTP_HEADER_ACCEPT_QUERY } from '../defs.js'
|
|
12
|
+
import { CacheControl } from '../headers/cache-control.js'
|
|
13
|
+
import { Conditional } from '../headers/conditional.js'
|
|
14
|
+
import { ContentRange } from '../headers/content-range.js'
|
|
15
|
+
import { CHARSET_UTF8 } from '../headers/content-type.js'
|
|
16
16
|
import {
|
|
17
17
|
coreHeaders,
|
|
18
18
|
customHeaders,
|
|
@@ -22,12 +22,10 @@ import {
|
|
|
22
22
|
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
23
23
|
/** @import { OutgoingHttpHeaders } from 'node:http2' */
|
|
24
24
|
/** @import { InputType } from 'node:zlib' */
|
|
25
|
-
/** @import { Metadata } from '
|
|
26
|
-
/** @import { EtagItem } from '../conditional.js' */
|
|
27
|
-
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
28
|
-
/** @import { ContentRangeDirective } from '../content-range.js' */
|
|
29
|
-
|
|
30
|
-
/** @typedef {ArrayBufferLike|ArrayBufferView|ReadableStream|string} SendBody */
|
|
25
|
+
/** @import { AcceptRangeUnits, Metadata, SendBody } from '../defs.js' */
|
|
26
|
+
/** @import { EtagItem, IMFFixDateInput } from '../headers/conditional.js' */
|
|
27
|
+
/** @import { CacheControlOptions } from '../headers/cache-control.js' */
|
|
28
|
+
/** @import { ContentRangeDirective } from '../headers/content-range.js' */
|
|
31
29
|
|
|
32
30
|
const {
|
|
33
31
|
HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
@@ -40,6 +38,7 @@ const {
|
|
|
40
38
|
HTTP2_HEADER_VARY,
|
|
41
39
|
HTTP2_HEADER_CACHE_CONTROL,
|
|
42
40
|
HTTP2_HEADER_ETAG,
|
|
41
|
+
HTTP2_HEADER_LAST_MODIFIED,
|
|
43
42
|
HTTP2_HEADER_AGE,
|
|
44
43
|
HTTP2_HEADER_ACCEPT_RANGES,
|
|
45
44
|
HTTP2_HEADER_CONTENT_RANGE,
|
|
@@ -49,7 +48,7 @@ const {
|
|
|
49
48
|
HTTP2_HEADER_RANGE
|
|
50
49
|
} = http2.constants
|
|
51
50
|
|
|
52
|
-
/** @typedef { (data: InputType) => Buffer } EncoderFun */
|
|
51
|
+
/** @typedef { (data: InputType) => Buffer<ArrayBuffer> } EncoderFun */
|
|
53
52
|
|
|
54
53
|
/** @type {Map<string, EncoderFun>} */
|
|
55
54
|
export const ENCODER_MAP = new Map([
|
|
@@ -66,17 +65,18 @@ export const ENCODER_MAP = new Map([
|
|
|
66
65
|
* @param {SendBody} body
|
|
67
66
|
* @param {string|undefined} encoding
|
|
68
67
|
* @param {EtagItem|undefined} etag
|
|
68
|
+
* @param {IMFFixDateInput|string|undefined} lastModified
|
|
69
69
|
* @param {number|undefined} age
|
|
70
70
|
* @param {CacheControlOptions|undefined} cacheControl
|
|
71
|
-
* @param {
|
|
71
|
+
* @param {AcceptRangeUnits|undefined} acceptRanges
|
|
72
72
|
* @param {Array<string>|undefined} supportedQueryTypes
|
|
73
73
|
* @param {Metadata} meta
|
|
74
74
|
*/
|
|
75
|
-
export function send_encoded(stream, status, contentType, body, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
75
|
+
export function send_encoded(stream, status, contentType, body, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
76
76
|
const obj = (typeof body === 'string') ? Buffer.from(body, CHARSET_UTF8) : body
|
|
77
77
|
|
|
78
78
|
const useIdentity = encoding === 'identity'
|
|
79
|
-
const encoder = encoding
|
|
79
|
+
const encoder = encoding === undefined ? undefined : ENCODER_MAP.get(encoding)
|
|
80
80
|
const hasEncoder = encoder !== undefined
|
|
81
81
|
const actualEncoding = hasEncoder ? encoding : undefined
|
|
82
82
|
|
|
@@ -88,7 +88,7 @@ export function send_encoded(stream, status, contentType, body, encoding, etag,
|
|
|
88
88
|
{ name: 'encode', duration: encodeEnd - encodeStart }
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
-
send_bytes(stream, status, contentType, encodedData, undefined, undefined, actualEncoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
|
|
91
|
+
send_bytes(stream, status, contentType, encodedData, undefined, undefined, actualEncoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta )
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
|
|
@@ -101,13 +101,14 @@ export function send_encoded(stream, status, contentType, body, encoding, etag,
|
|
|
101
101
|
* @param {number|undefined} contentLength
|
|
102
102
|
* @param {string|undefined} encoding
|
|
103
103
|
* @param {EtagItem|undefined} etag
|
|
104
|
+
* @param {IMFFixDateInput|string|undefined} lastModified
|
|
104
105
|
* @param {number|undefined} age
|
|
105
106
|
* @param {CacheControlOptions|undefined} cacheControl
|
|
106
|
-
* @param {
|
|
107
|
+
* @param {AcceptRangeUnits|undefined} acceptRanges
|
|
107
108
|
* @param {Array<string>|undefined} supportedQueryTypes
|
|
108
109
|
* @param {Metadata} meta
|
|
109
110
|
*/
|
|
110
|
-
export function send_bytes(stream, status, contentType, obj, range, contentLength, encoding, etag, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
111
|
+
export function send_bytes(stream, status, contentType, obj, range, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryTypes, meta) {
|
|
111
112
|
const contentLen = Number.isInteger(contentLength) ? `${contentLength}` : undefined
|
|
112
113
|
const supportsQuery = supportedQueryTypes !== undefined && supportedQueryTypes.length > 0
|
|
113
114
|
|
|
@@ -125,7 +126,8 @@ export function send_bytes(stream, status, contentType, obj, range, contentLengt
|
|
|
125
126
|
[HTTP2_HEADER_VARY]: varyHeaders.join(','),
|
|
126
127
|
[HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
|
|
127
128
|
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
128
|
-
[
|
|
129
|
+
[HTTP2_HEADER_LAST_MODIFIED]: Conditional.encodeFixDate(lastModified),
|
|
130
|
+
[HTTP2_HEADER_AGE]: age === undefined ? undefined : `${age}`,
|
|
129
131
|
[HTTP2_HEADER_CONTENT_LENGTH]: contentLen,
|
|
130
132
|
[HTTP2_HEADER_CONTENT_RANGE]: ContentRange.encode(range),
|
|
131
133
|
[HTTP2_HEADER_ACCEPT_RANGES]: acceptRanges,
|
|
@@ -166,7 +168,16 @@ export function send(stream, status, headers, exposedHeaders, contentType, body,
|
|
|
166
168
|
|
|
167
169
|
if(stream.writable && body !== undefined) {
|
|
168
170
|
if(body instanceof ReadableStream) {
|
|
169
|
-
|
|
171
|
+
const signal = undefined // AbortSignal.timeout(1000)
|
|
172
|
+
pipeline(
|
|
173
|
+
Readable.fromWeb(body, { signal }),
|
|
174
|
+
stream,
|
|
175
|
+
err => {
|
|
176
|
+
if(err !== null) {
|
|
177
|
+
console.warn('pipeline error')
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
170
181
|
return
|
|
171
182
|
}
|
|
172
183
|
|
package/src/accept.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { parseAcceptStyleHeader } from './accept-util.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @import { AcceptStyleItem } from './accept-util.js'
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {Object} AcceptExtensionItem
|
|
9
|
-
* @property {string} mimetype
|
|
10
|
-
* @property {string} type
|
|
11
|
-
* @property {string} subtype
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {AcceptStyleItem & AcceptExtensionItem} AcceptItem
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export const ACCEPT_SEPARATOR = { SUBTYPE: '/' }
|
|
19
|
-
export const ACCEPT_ANY = '*'
|
|
20
|
-
|
|
21
|
-
export const WELL_KNOWN = new Map([
|
|
22
|
-
[ '*/*', [ { name: '*/*', quality: 1 } ] ],
|
|
23
|
-
[ 'application/json', [ { name: 'application/json', quality: 1 } ] ]
|
|
24
|
-
])
|
|
25
|
-
|
|
26
|
-
export class Accept {
|
|
27
|
-
/**
|
|
28
|
-
* @param {string|undefined} acceptHeader
|
|
29
|
-
* @returns {Array<AcceptItem>}
|
|
30
|
-
*/
|
|
31
|
-
static parse(acceptHeader) {
|
|
32
|
-
return parseAcceptStyleHeader(acceptHeader, WELL_KNOWN)
|
|
33
|
-
.map(({ name, quality, parameters }) => {
|
|
34
|
-
const [ type, subtype ] = name
|
|
35
|
-
.split(ACCEPT_SEPARATOR.SUBTYPE)
|
|
36
|
-
.map(t => t.trim())
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
mimetype: `${type}${ACCEPT_SEPARATOR.SUBTYPE}${subtype ?? ACCEPT_ANY}`,
|
|
40
|
-
name, type, subtype,
|
|
41
|
-
quality,
|
|
42
|
-
parameters
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
.sort((entryA, entryB) => {
|
|
46
|
-
if(entryA.quality === entryB.quality) {
|
|
47
|
-
// prefer things with less ANY
|
|
48
|
-
const specificityA = (entryA.type === ACCEPT_ANY ? 1 : 0) + (entryA.subtype === ACCEPT_ANY ? 1 : 0)
|
|
49
|
-
const specificityB = (entryB.type === ACCEPT_ANY ? 1 : 0) + (entryB.subtype === ACCEPT_ANY ? 1 : 0)
|
|
50
|
-
return specificityA - specificityB
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// B - A descending order
|
|
54
|
-
const qualityB = entryB.quality ?? 0
|
|
55
|
-
const qualityA = entryA.quality ?? 0
|
|
56
|
-
return qualityB - qualityA
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {string|undefined} acceptHeader
|
|
62
|
-
* @param {Array<string>} supportedTypes
|
|
63
|
-
*/
|
|
64
|
-
static select(acceptHeader, supportedTypes) {
|
|
65
|
-
const accepts = Accept.parse(acceptHeader)
|
|
66
|
-
return Accept.selectFrom(accepts, supportedTypes)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {Array<AcceptItem>} accepts
|
|
71
|
-
* @param {Array<string>} supportedTypes
|
|
72
|
-
*/
|
|
73
|
-
static selectFrom(accepts, supportedTypes) {
|
|
74
|
-
const bests = accepts.map(accept => {
|
|
75
|
-
const { type, subtype, quality } = accept
|
|
76
|
-
const st = supportedTypes.filter(supportedType => {
|
|
77
|
-
const [ stType, stSubtype ] = supportedType.split(ACCEPT_SEPARATOR.SUBTYPE)
|
|
78
|
-
return ((stType === type || type === ACCEPT_ANY) && (stSubtype === subtype || subtype === ACCEPT_ANY))
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
supportedTypes: st,
|
|
83
|
-
quality
|
|
84
|
-
}
|
|
85
|
-
})
|
|
86
|
-
.filter(best => best.supportedTypes.length > 0)
|
|
87
|
-
|
|
88
|
-
if(bests.length === 0) { return undefined }
|
|
89
|
-
const [ first ] = bests
|
|
90
|
-
if(first === undefined) { return undefined }
|
|
91
|
-
const [ firstSt ] = first.supportedTypes
|
|
92
|
-
return firstSt
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// console.log(Accept.parse('text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, text/*;q=.8, */*;q=0.7'))
|
|
97
|
-
// console.log(Accept.select('text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, text/*;q=.8, */*;q=0.7', [ 'application/json', 'text/plain' ]))
|
|
98
|
-
|
|
99
|
-
// const tests = [
|
|
100
|
-
// undefined,
|
|
101
|
-
// '',
|
|
102
|
-
// ' ',
|
|
103
|
-
// ' fake',
|
|
104
|
-
// ' application/json',
|
|
105
|
-
// ' application/xml,',
|
|
106
|
-
// ' ,application/xml ,,',
|
|
107
|
-
// ' audio/*; q=0.2, audio/basic',
|
|
108
|
-
// ' text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8',
|
|
109
|
-
// ' text/*;q=0.3, text/plain;q=0.7, text/plain;format=flowed,\ntext/plain;format=fixed;q=0.4, */*;q=0.5',
|
|
110
|
-
|
|
111
|
-
// ' */*, foo/bar, foo/*, biz/bang, */*;q=.2, quix/quak;q=.1',
|
|
112
|
-
// 'foo / bar ; q = .5'
|
|
113
|
-
// ]
|
|
114
|
-
|
|
115
|
-
// tests.forEach(test => {
|
|
116
|
-
// const result = Accept.parse(test)
|
|
117
|
-
// console.log('=============================')
|
|
118
|
-
// console.log({ test })
|
|
119
|
-
// console.log('---')
|
|
120
|
-
// console.log(result)
|
|
121
|
-
// })
|
|
122
|
-
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {Object} Disposition
|
|
3
|
-
* @property {string} disposition
|
|
4
|
-
* @property {Map<string, string|undefined>} parameters
|
|
5
|
-
* @property {string|undefined} [name]
|
|
6
|
-
* @property {string|undefined} [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|undefined} 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
|
-
if(disposition === undefined) { return undefined }
|
|
26
|
-
const parameters = new Map(parameterSet.map(parameter => {
|
|
27
|
-
const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
|
|
28
|
-
if(key === undefined) { return undefined }
|
|
29
|
-
return { key, value }
|
|
30
|
-
})
|
|
31
|
-
.filter(item => item !== undefined)
|
|
32
|
-
.map(({ key, value }) => ([ key, value ])))
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const name = parameters.get(DISPOSITION_PARAM_NAME)
|
|
36
|
-
const filename = parameters.get(DISPOSITION_PARAM_FILENAME)
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
disposition,
|
|
40
|
-
parameters,
|
|
41
|
-
name, filename
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// console.log(parseContentDisposition())
|
|
46
|
-
// // console.log(parseContentDisposition(null))
|
|
47
|
-
// console.log(parseContentDisposition(''))
|
|
48
|
-
// console.log(parseContentDisposition('form-data'))
|
|
49
|
-
// console.log(parseContentDisposition(' form-data ; name'))
|
|
50
|
-
// console.log(parseContentDisposition('form-data; name="key"'))
|
|
51
|
-
|
|
52
|
-
// console.log(parseContentDisposition('inline'))
|
|
53
|
-
// console.log(parseContentDisposition('attachment'))
|
|
54
|
-
// console.log(parseContentDisposition('attachment; filename="file name.jpg"'))
|
|
55
|
-
// console.log(parseContentDisposition('attachment; filename*=UTF-8\'\'file%20name.jpg'))
|
|
56
|
-
// console.log(parseContentDisposition('attachment; filename*=UTF-8\'\'file%20name.jpg'))
|
|
57
|
-
// console.log(parseContentDisposition('form-data;title*=us-ascii\'en-us\'This%20is%20%2A%2A%2Afun%2A%2A%2A'))
|
package/src/content-type.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
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_MULTIPART_RANGE = 'multipart/byteranges'
|
|
9
|
-
export const MIME_TYPE_OCTET_STREAM = 'application/octet-stream'
|
|
10
|
-
export const MIME_TYPE_MESSAGE_HTTP = 'message/http'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export const KNOWN_CONTENT_TYPES = [
|
|
14
|
-
'application', 'audio', 'image', 'message',
|
|
15
|
-
'multipart','text', 'video'
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
export const TYPE_X_TOKEN_PREFIX = 'X-'
|
|
19
|
-
|
|
20
|
-
export const SPECIAL_CHARS = [
|
|
21
|
-
// special
|
|
22
|
-
'(', ')', '<', '>',
|
|
23
|
-
'@', ',', ';', ':',
|
|
24
|
-
'\\', '"', '/', '[',
|
|
25
|
-
']', '?', '.', '=',
|
|
26
|
-
// space
|
|
27
|
-
' ', '\u000B', '\u000C',
|
|
28
|
-
// control
|
|
29
|
-
'\n', '\r', '\t'
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
export const WHITESPACE_REGEX = /\s/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @param {string} c
|
|
36
|
-
*/
|
|
37
|
-
export function isWhitespace(c){ return WHITESPACE_REGEX.test(c) }
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @param {string|undefined} value
|
|
41
|
-
*/
|
|
42
|
-
export function hasSpecialChar(value) {
|
|
43
|
-
if(value === undefined) { return false }
|
|
44
|
-
for(const special of SPECIAL_CHARS) {
|
|
45
|
-
if(value.includes(special)) { return true}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return false
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @typedef {Object} ContentType
|
|
53
|
-
* @property {string} mimetype
|
|
54
|
-
* @property {string} mimetypeRaw
|
|
55
|
-
* @property {string} type
|
|
56
|
-
* @property {string} subtype
|
|
57
|
-
* @property {string} [charset]
|
|
58
|
-
* @property {Map<string, string>} parameters
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
|
-
export const CONTENT_TYPE_SEPARATOR = {
|
|
62
|
-
SUBTYPE: '/',
|
|
63
|
-
PARAMETER: ';',
|
|
64
|
-
KVP: '='
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export const CHARSET_UTF8 = 'utf8'
|
|
68
|
-
export const CHARSET = 'charset'
|
|
69
|
-
export const PARAMETER_CHARSET_UTF8 = `${CHARSET}${CONTENT_TYPE_SEPARATOR.KVP}${CHARSET_UTF8}`
|
|
70
|
-
export const CONTENT_TYPE_JSON = `${MIME_TYPE_JSON}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
|
|
71
|
-
export const CONTENT_TYPE_TEXT = `${MIME_TYPE_TEXT}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
|
|
72
|
-
export const CONTENT_TYPE_MESSAGE_HTTP = `${MIME_TYPE_MESSAGE_HTTP}${CONTENT_TYPE_SEPARATOR.PARAMETER}${PARAMETER_CHARSET_UTF8}`
|
|
73
|
-
|
|
74
|
-
/** @type {ContentType} */
|
|
75
|
-
export const WELL_KNOWN_JSON = {
|
|
76
|
-
mimetype: 'application/json',
|
|
77
|
-
mimetypeRaw: 'application/json',
|
|
78
|
-
type: 'application',
|
|
79
|
-
subtype: 'json',
|
|
80
|
-
charset: 'utf8',
|
|
81
|
-
parameters: new Map()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export const WELL_KNOWN_CONTENT_TYPES = new Map([
|
|
85
|
-
[ 'application/json', WELL_KNOWN_JSON ],
|
|
86
|
-
[ 'application/json;charset=utf8', WELL_KNOWN_JSON ]
|
|
87
|
-
])
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* @param {string|undefined} contentTypeHeader
|
|
92
|
-
* @returns {ContentType|undefined}
|
|
93
|
-
*/
|
|
94
|
-
export function parseContentType(contentTypeHeader) {
|
|
95
|
-
if(contentTypeHeader === undefined) { return undefined }
|
|
96
|
-
if(contentTypeHeader === null) { return undefined }
|
|
97
|
-
|
|
98
|
-
const wellKnown = WELL_KNOWN_CONTENT_TYPES.get(contentTypeHeader)
|
|
99
|
-
if(wellKnown !== undefined) { return wellKnown }
|
|
100
|
-
|
|
101
|
-
const [ mimetypeRaw, ...parameterSet ] = contentTypeHeader.split(CONTENT_TYPE_SEPARATOR.PARAMETER)
|
|
102
|
-
if(mimetypeRaw === undefined) { return undefined }
|
|
103
|
-
if(mimetypeRaw === '') { return undefined }
|
|
104
|
-
|
|
105
|
-
const [ typeRaw, subtypeRaw ] = mimetypeRaw
|
|
106
|
-
.split(CONTENT_TYPE_SEPARATOR.SUBTYPE)
|
|
107
|
-
.map(t => t.toLowerCase())
|
|
108
|
-
|
|
109
|
-
if(typeRaw === undefined) { return undefined }
|
|
110
|
-
if(typeRaw === '') { return undefined }
|
|
111
|
-
if(hasSpecialChar(typeRaw)) { return undefined }
|
|
112
|
-
if(subtypeRaw === undefined) { return undefined }
|
|
113
|
-
if(subtypeRaw === '') { return undefined }
|
|
114
|
-
if(hasSpecialChar(subtypeRaw)) { return undefined }
|
|
115
|
-
|
|
116
|
-
const type = typeRaw.trim()
|
|
117
|
-
const subtype = subtypeRaw.trim()
|
|
118
|
-
|
|
119
|
-
const parameters = new Map()
|
|
120
|
-
|
|
121
|
-
for(const parameter of parameterSet) {
|
|
122
|
-
const [ key, value ] = parameter.split(CONTENT_TYPE_SEPARATOR.KVP)
|
|
123
|
-
if(key === undefined || key === '') { continue }
|
|
124
|
-
if(value === undefined || value === '') { continue }
|
|
125
|
-
|
|
126
|
-
const actualKey = key?.trim().toLowerCase()
|
|
127
|
-
if(hasSpecialChar(actualKey)) { continue }
|
|
128
|
-
|
|
129
|
-
const quoted = (value.at(0) === '"' && value.at(-1) === '"')
|
|
130
|
-
const actualValue = quoted ? value.substring(1, value.length - 1) : value
|
|
131
|
-
|
|
132
|
-
if(!parameters.has(actualKey)) {
|
|
133
|
-
parameters.set(actualKey, actualValue)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const charset = parameters.get(CHARSET)
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
mimetype: `${type}${CONTENT_TYPE_SEPARATOR.SUBTYPE}${subtype}`,
|
|
141
|
-
mimetypeRaw, type, subtype,
|
|
142
|
-
charset,
|
|
143
|
-
parameters
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
// console.log(parseContentType('multipart/form-data; boundary=----WebKitFormBoundaryJZy5maoMBkBMoGjt'))
|
package/src/link.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {Object} LinkItem
|
|
3
|
-
* @property {string} url
|
|
4
|
-
* @property {string|undefined} [relation]
|
|
5
|
-
* @property {Map<string, string>|undefined} [parameters]
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export class Link {
|
|
9
|
-
/**
|
|
10
|
-
* @param {LinkItem} link
|
|
11
|
-
*/
|
|
12
|
-
static *#encode(link) {
|
|
13
|
-
const encodedUri = encodeURI(link.url)
|
|
14
|
-
|
|
15
|
-
yield `<${encodedUri}>`
|
|
16
|
-
if(link.relation !== undefined) { yield `rel="${link.relation}"` }
|
|
17
|
-
if(link.parameters === undefined) { return }
|
|
18
|
-
for(const [ key, value ] of link.parameters) {
|
|
19
|
-
yield `${key}="${value}"`
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {LinkItem} link
|
|
26
|
-
*/
|
|
27
|
-
static encode(link) {
|
|
28
|
-
return [ ...Link.#encode(link) ].join('; ')
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// console.log(Link.encode({ url: '/index.html', parameters: new Map([ [ 'as', 'style' ], [ 'fetchpriority', 'high' ] ]) }))
|
|
33
|
-
// console.log(Link.encode({ url: '/index.html', relation: 'next', parameters: new Map([ [ 'fetchpriority', 'high' ] ]) }))
|
|
34
|
-
// console.log(Link.encode({ url: '/index.html', relation: 'next' }))
|
|
35
|
-
// console.log(Link.encode({ url: 'https://example.com/苗条', relation: 'preconnect' }))
|
package/src/response/bytes.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import http2 from 'node:http2'
|
|
2
|
-
|
|
3
|
-
import { send_bytes } from './send-util.js'
|
|
4
|
-
|
|
5
|
-
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
|
-
/** @import { Metadata } from './defs.js' */
|
|
7
|
-
/** @import { EtagItem } from '../conditional.js' */
|
|
8
|
-
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
9
|
-
/** @import { SendBody } from './send-util.js' */
|
|
10
|
-
|
|
11
|
-
const { HTTP_STATUS_OK } = http2.constants
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {ServerHttp2Stream} stream
|
|
15
|
-
* @param {SendBody|undefined} obj
|
|
16
|
-
* @param {string|undefined} contentType
|
|
17
|
-
* @param {number|undefined} contentLength
|
|
18
|
-
* @param {string|undefined} encoding
|
|
19
|
-
* @param {EtagItem|undefined} etag
|
|
20
|
-
* @param {number|undefined} age
|
|
21
|
-
* @param {CacheControlOptions} cacheControl
|
|
22
|
-
* @param {'bytes'|'none'|undefined} acceptRanges
|
|
23
|
-
* @param {Metadata} meta
|
|
24
|
-
*/
|
|
25
|
-
export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, age, cacheControl, acceptRanges, meta) {
|
|
26
|
-
send_bytes(stream, HTTP_STATUS_OK, contentType, obj, undefined, contentLength, encoding, etag, age, cacheControl, acceptRanges, undefined, meta)
|
|
27
|
-
}
|
package/src/response/created.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import http2 from 'node:http2'
|
|
2
|
-
|
|
3
|
-
import { Conditional } from '../conditional.js'
|
|
4
|
-
import { send } from './send-util.js'
|
|
5
|
-
|
|
6
|
-
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
|
-
/** @import { Metadata } from './defs.js' */
|
|
8
|
-
/** @import { EtagItem } from '../conditional.js' */
|
|
9
|
-
|
|
10
|
-
const {
|
|
11
|
-
HTTP2_HEADER_LOCATION,
|
|
12
|
-
HTTP2_HEADER_ETAG
|
|
13
|
-
} = http2.constants
|
|
14
|
-
|
|
15
|
-
const { HTTP_STATUS_CREATED } = http2.constants
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @param {ServerHttp2Stream} stream
|
|
19
|
-
* @param {URL} location
|
|
20
|
-
* @param {EtagItem|undefined} etag
|
|
21
|
-
* @param {Metadata} meta
|
|
22
|
-
*/
|
|
23
|
-
export function sendCreated(stream, location, etag, meta) {
|
|
24
|
-
send(stream, HTTP_STATUS_CREATED, {
|
|
25
|
-
[HTTP2_HEADER_LOCATION]: location.href,
|
|
26
|
-
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
|
|
27
|
-
}, [ HTTP2_HEADER_LOCATION ], undefined, undefined, meta)
|
|
28
|
-
}
|
package/src/response/json.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import http2 from 'node:http2'
|
|
2
|
-
|
|
3
|
-
import { CONTENT_TYPE_JSON } from '../content-type.js'
|
|
4
|
-
import { send_encoded } from './send-util.js'
|
|
5
|
-
|
|
6
|
-
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
|
-
/** @import { Metadata } from './defs.js' */
|
|
8
|
-
/** @import { EtagItem } from '../conditional.js' */
|
|
9
|
-
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
10
|
-
|
|
11
|
-
const { HTTP_STATUS_OK } = http2.constants
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @param {ServerHttp2Stream} stream
|
|
15
|
-
* @param {Object} obj
|
|
16
|
-
* @param {string|undefined} encoding
|
|
17
|
-
* @param {EtagItem|undefined} etag
|
|
18
|
-
* @param {number|undefined} age
|
|
19
|
-
* @param {CacheControlOptions} cacheControl
|
|
20
|
-
* @param {Array<string>|undefined} supportedQueryTypes
|
|
21
|
-
* @param {Metadata} meta
|
|
22
|
-
*/
|
|
23
|
-
export function sendJSON_Encoded(stream, obj, encoding, etag, age, cacheControl, supportedQueryTypes, meta) {
|
|
24
|
-
if(stream.closed) { return }
|
|
25
|
-
|
|
26
|
-
const json = JSON.stringify(obj)
|
|
27
|
-
send_encoded(stream, HTTP_STATUS_OK, CONTENT_TYPE_JSON, json, encoding, etag, age, cacheControl, undefined, supportedQueryTypes, meta)
|
|
28
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import http2 from 'node:http2'
|
|
2
|
-
|
|
3
|
-
import { Conditional } from '../conditional.js'
|
|
4
|
-
import { send } from './send-util.js'
|
|
5
|
-
|
|
6
|
-
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
7
|
-
/** @import { Metadata } from './defs.js' */
|
|
8
|
-
/** @import { EtagItem } from '../conditional.js' */
|
|
9
|
-
|
|
10
|
-
const {
|
|
11
|
-
HTTP2_HEADER_ETAG
|
|
12
|
-
} = http2.constants
|
|
13
|
-
|
|
14
|
-
const { HTTP_STATUS_NO_CONTENT } = http2.constants
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @param {ServerHttp2Stream} stream
|
|
18
|
-
* @param {EtagItem|undefined} etag
|
|
19
|
-
* @param {Metadata} meta
|
|
20
|
-
*/
|
|
21
|
-
export function sendNoContent(stream, etag, meta) {
|
|
22
|
-
send(stream, HTTP_STATUS_NO_CONTENT, {
|
|
23
|
-
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag)
|
|
24
|
-
}, [], undefined, undefined, meta)
|
|
25
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import http2 from 'node:http2'
|
|
2
|
-
|
|
3
|
-
import { send } from './send-util.js'
|
|
4
|
-
|
|
5
|
-
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
6
|
-
/** @import { Metadata } from './defs.js' */
|
|
7
|
-
|
|
8
|
-
const {
|
|
9
|
-
HTTP_STATUS_METHOD_NOT_ALLOWED
|
|
10
|
-
} = http2.constants
|
|
11
|
-
|
|
12
|
-
const { HTTP2_HEADER_ALLOW } = http2.constants
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @param {ServerHttp2Stream} stream
|
|
16
|
-
* @param {Array<string>} methods
|
|
17
|
-
* @param {Metadata} meta
|
|
18
|
-
*/
|
|
19
|
-
export function sendNotAllowed(stream, methods, meta) {
|
|
20
|
-
send(stream, HTTP_STATUS_METHOD_NOT_ALLOWED, {
|
|
21
|
-
[HTTP2_HEADER_ALLOW]: methods.join(',')
|
|
22
|
-
}, [ HTTP2_HEADER_ALLOW ], undefined, undefined, meta)
|
|
23
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import http2 from 'node:http2'
|
|
2
|
-
|
|
3
|
-
import { CacheControl } from '../cache-control.js'
|
|
4
|
-
import { Conditional } from '../conditional.js'
|
|
5
|
-
import { send } from './send-util.js'
|
|
6
|
-
|
|
7
|
-
/** @import { ServerHttp2Stream } from 'node:http2' */
|
|
8
|
-
/** @import { Metadata } from './defs.js' */
|
|
9
|
-
/** @import { EtagItem } from '../conditional.js' */
|
|
10
|
-
/** @import { CacheControlOptions } from '../cache-control.js' */
|
|
11
|
-
|
|
12
|
-
const {
|
|
13
|
-
HTTP2_HEADER_AGE,
|
|
14
|
-
HTTP2_HEADER_ETAG,
|
|
15
|
-
HTTP2_HEADER_VARY,
|
|
16
|
-
HTTP2_HEADER_CACHE_CONTROL
|
|
17
|
-
} = http2.constants
|
|
18
|
-
|
|
19
|
-
const { HTTP_STATUS_NOT_MODIFIED } = http2.constants
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {ServerHttp2Stream} stream
|
|
23
|
-
* @param {EtagItem|undefined} etag
|
|
24
|
-
* @param {number|undefined} age
|
|
25
|
-
* @param {CacheControlOptions} cacheControl
|
|
26
|
-
* @param {Metadata} meta
|
|
27
|
-
*/
|
|
28
|
-
export function sendNotModified(stream, etag, age, cacheControl, meta) {
|
|
29
|
-
send(stream, HTTP_STATUS_NOT_MODIFIED, {
|
|
30
|
-
[HTTP2_HEADER_VARY]: 'Accept, Accept-Encoding',
|
|
31
|
-
[HTTP2_HEADER_CACHE_CONTROL]: CacheControl.encode(cacheControl),
|
|
32
|
-
[HTTP2_HEADER_ETAG]: Conditional.encodeEtag(etag),
|
|
33
|
-
[HTTP2_HEADER_AGE]: age !== undefined ? `${age}` : undefined
|
|
34
|
-
}, [ HTTP2_HEADER_AGE ], undefined, undefined, meta)
|
|
35
|
-
}
|