starlight-server 0.0.1
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 +1 -0
- package/dist/demo/index.d.ts +1 -0
- package/dist/demo/index.js +27 -0
- package/dist/http/body/form-data.d.ts +35 -0
- package/dist/http/body/form-data.js +141 -0
- package/dist/http/body/index.d.ts +23 -0
- package/dist/http/body/index.js +47 -0
- package/dist/http/body/receive.d.ts +7 -0
- package/dist/http/body/receive.js +39 -0
- package/dist/http/http-status.d.ts +9 -0
- package/dist/http/http-status.js +64 -0
- package/dist/http/index.d.ts +9 -0
- package/dist/http/index.js +9 -0
- package/dist/http/mime-types.d.ts +14 -0
- package/dist/http/mime-types.js +764 -0
- package/dist/http/request.d.ts +25 -0
- package/dist/http/request.js +40 -0
- package/dist/http/response.d.ts +32 -0
- package/dist/http/response.js +66 -0
- package/dist/http/server.d.ts +31 -0
- package/dist/http/server.js +52 -0
- package/dist/http/types.d.ts +26 -0
- package/dist/http/types.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/logging.d.ts +24 -0
- package/dist/logging.js +30 -0
- package/dist/router/cors.d.ts +24 -0
- package/dist/router/cors.js +35 -0
- package/dist/router/index.d.ts +38 -0
- package/dist/router/index.js +36 -0
- package/dist/router/match.d.ts +23 -0
- package/dist/router/match.js +172 -0
- package/dist/router/parameters.d.ts +51 -0
- package/dist/router/parameters.js +118 -0
- package/dist/router/router.d.ts +127 -0
- package/dist/router/router.js +97 -0
- package/dist/swagger/index.d.ts +1 -0
- package/dist/swagger/index.js +168 -0
- package/dist/swagger/openapi-spec.d.ts +261 -0
- package/dist/swagger/openapi-spec.js +5 -0
- package/dist/validators/array.d.ts +9 -0
- package/dist/validators/array.js +28 -0
- package/dist/validators/boolean.d.ts +4 -0
- package/dist/validators/boolean.js +28 -0
- package/dist/validators/common.d.ts +23 -0
- package/dist/validators/common.js +25 -0
- package/dist/validators/index.d.ts +20 -0
- package/dist/validators/index.js +38 -0
- package/dist/validators/number.d.ts +10 -0
- package/dist/validators/number.js +30 -0
- package/dist/validators/object.d.ts +13 -0
- package/dist/validators/object.js +36 -0
- package/dist/validators/string.d.ts +11 -0
- package/dist/validators/string.js +29 -0
- package/package.json +54 -0
- package/src/demo/index.ts +33 -0
- package/src/http/body/form-data.ts +164 -0
- package/src/http/body/index.ts +59 -0
- package/src/http/body/receive.ts +49 -0
- package/src/http/http-status.ts +65 -0
- package/src/http/index.ts +9 -0
- package/src/http/mime-types.ts +765 -0
- package/src/http/request.ts +44 -0
- package/src/http/response.ts +73 -0
- package/src/http/server.ts +67 -0
- package/src/http/types.ts +31 -0
- package/src/index.ts +4 -0
- package/src/logging.ts +57 -0
- package/src/router/cors.ts +54 -0
- package/src/router/index.ts +38 -0
- package/src/router/match.ts +194 -0
- package/src/router/parameters.ts +172 -0
- package/src/router/router.ts +233 -0
- package/src/swagger/index.ts +184 -0
- package/src/swagger/openapi-spec.ts +312 -0
- package/src/validators/array.ts +33 -0
- package/src/validators/boolean.ts +23 -0
- package/src/validators/common.ts +46 -0
- package/src/validators/index.ts +50 -0
- package/src/validators/number.ts +36 -0
- package/src/validators/object.ts +41 -0
- package/src/validators/string.ts +38 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { success, failed } from '@anjianshi/utils';
|
|
2
|
+
import isPlainObject from 'lodash/isPlainObject.js';
|
|
3
|
+
import { Validator } from './common.js';
|
|
4
|
+
export class ObjectValidator extends Validator {
|
|
5
|
+
validate(fieldName, value) {
|
|
6
|
+
const superResult = super.validate(fieldName, value);
|
|
7
|
+
if (!superResult.success)
|
|
8
|
+
return superResult;
|
|
9
|
+
value = superResult.data;
|
|
10
|
+
if (value === null || value === undefined)
|
|
11
|
+
return superResult;
|
|
12
|
+
const opt = this.options;
|
|
13
|
+
if (!isPlainObject(value))
|
|
14
|
+
return failed(`${fieldName} should be a plain object`);
|
|
15
|
+
const formatted = {};
|
|
16
|
+
if ('struct' in opt) {
|
|
17
|
+
for (const [key, itemValidator] of Object.entries(opt.struct)) {
|
|
18
|
+
const itemResult = itemValidator.validate(`${fieldName}["${key}"]`, value[key]);
|
|
19
|
+
if (itemResult.success)
|
|
20
|
+
formatted[key] = itemResult.data;
|
|
21
|
+
else
|
|
22
|
+
return itemResult;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
for (const [key, itemValue] of Object.entries(value)) {
|
|
27
|
+
const itemResult = opt.value.validate(`${fieldName}["${key}"]`, itemValue);
|
|
28
|
+
if (itemResult.success)
|
|
29
|
+
formatted[key] = itemResult.data;
|
|
30
|
+
else
|
|
31
|
+
return itemResult;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return success(formatted);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Validator, type CommonOptions } from './common.js';
|
|
2
|
+
export interface StringOptions {
|
|
3
|
+
min?: number;
|
|
4
|
+
max?: number;
|
|
5
|
+
pattern?: RegExp;
|
|
6
|
+
trim: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class StringValidator extends Validator<StringOptions> {
|
|
9
|
+
constructor(options?: Partial<StringOptions> & Partial<CommonOptions>);
|
|
10
|
+
validate(fieldName: string, value: unknown): import("@anjianshi/utils").Success<unknown> | import("@anjianshi/utils").Failed<string>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { success, failed } from '@anjianshi/utils';
|
|
2
|
+
import { Validator } from './common.js';
|
|
3
|
+
export class StringValidator extends Validator {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super({
|
|
6
|
+
trim: true,
|
|
7
|
+
...(options ?? {}),
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
validate(fieldName, value) {
|
|
11
|
+
const superResult = super.validate(fieldName, value);
|
|
12
|
+
if (!superResult.success)
|
|
13
|
+
return superResult;
|
|
14
|
+
value = superResult.data;
|
|
15
|
+
if (value === null || value === undefined)
|
|
16
|
+
return superResult;
|
|
17
|
+
const opt = this.options;
|
|
18
|
+
if (typeof value !== 'string')
|
|
19
|
+
return failed(`${fieldName} should be a string`);
|
|
20
|
+
const formatted = opt.trim ? value.trim() : value;
|
|
21
|
+
if (typeof opt.min === 'number' && formatted.length < opt.min)
|
|
22
|
+
return failed(`${fieldName}'s length must >= ${opt.min}`);
|
|
23
|
+
if (typeof opt.max === 'number' && formatted.length > opt.max)
|
|
24
|
+
return failed(`${fieldName}'s length must <= ${opt.max}`);
|
|
25
|
+
if (opt.pattern && !opt.pattern.exec(formatted))
|
|
26
|
+
return failed(`${fieldName} does not match the pattern.`);
|
|
27
|
+
return success(formatted);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "starlight-server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Simple But Powerful Node.js HTTP Server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"watch": "rimraf dist && tsc && (concurrently \"tsc -w\" \"tsc-alias -w\")",
|
|
8
|
+
"build": "rimraf dist && tsc && tsc-alias",
|
|
9
|
+
"demo": "node dist/demo/index.js",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"web",
|
|
14
|
+
"app",
|
|
15
|
+
"http",
|
|
16
|
+
"framework"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/anjianshi/starlight-server",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/anjianshi/starlight-server/issues",
|
|
21
|
+
"email": "anjianshi@gmail.com"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src/",
|
|
25
|
+
"dist/"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "anjianshi <anjianshi@gmail.com>",
|
|
29
|
+
"repository": "github:anjianshi/starlight-server",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@anjianshi/utils": "^1.0.9",
|
|
35
|
+
"chalk": "^5.3.0",
|
|
36
|
+
"debug": "^4.3.4",
|
|
37
|
+
"lodash": "^4.17.21",
|
|
38
|
+
"swagger-ui-dist": "^5.10.3"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@anjianshi/presets": "^1.4.0",
|
|
42
|
+
"@types/debug": "^4.1.9",
|
|
43
|
+
"@types/lodash": "^4.14.199",
|
|
44
|
+
"@types/node": "^20.8.6",
|
|
45
|
+
"@types/swagger-ui-dist": "^3.30.4",
|
|
46
|
+
"concurrently": "^8.2.1",
|
|
47
|
+
"eslint": "^8.54.0",
|
|
48
|
+
"rimraf": "^4.3.0",
|
|
49
|
+
"tsc-alias": "^1.8.8",
|
|
50
|
+
"typescript": "^5.3.2"
|
|
51
|
+
},
|
|
52
|
+
"eslintIgnore": [],
|
|
53
|
+
"prettier": "@anjianshi/presets/prettierrc"
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { getFileDir } from '@anjianshi/utils/env-node/index.js'
|
|
3
|
+
import { getLogger, startHTTPServer, DefaultRouter } from '@/index.js'
|
|
4
|
+
// import { registerSwaggerRoute } from '@/swagger/index.js'
|
|
5
|
+
|
|
6
|
+
const logger = getLogger({
|
|
7
|
+
level: 'debug',
|
|
8
|
+
debugLib: '*',
|
|
9
|
+
file: {
|
|
10
|
+
dir: path.resolve(getFileDir(import.meta), '../../logs'),
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const router = new DefaultRouter()
|
|
15
|
+
|
|
16
|
+
router.registerResponseReference('hello', { object: [{ name: 'hello', type: 'string' }] })
|
|
17
|
+
|
|
18
|
+
router.register({
|
|
19
|
+
method: 'GET',
|
|
20
|
+
path: '/hello',
|
|
21
|
+
response: { ref: 'hello' },
|
|
22
|
+
handler({ response }) {
|
|
23
|
+
response.json({ hello: 'world' })
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// registerSwaggerRoute(router)
|
|
28
|
+
|
|
29
|
+
startHTTPServer({
|
|
30
|
+
handler: router.handle,
|
|
31
|
+
logger: logger.getChild('http'),
|
|
32
|
+
port: 8801,
|
|
33
|
+
})
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing multipart/form-data
|
|
3
|
+
*
|
|
4
|
+
* Referenced this library:https://github.com/nachomazzara/parse-multipart-data
|
|
5
|
+
* (This library has bugs, so I decided to implement it myself.)
|
|
6
|
+
*
|
|
7
|
+
* Introduce for multipart/form-data format:https://www.jianshu.com/p/29e38bcc8a1d
|
|
8
|
+
*/
|
|
9
|
+
import { HTTPError } from '../types.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 解析后的 FormData(格式参考浏览器环境中的 FormData 对象)
|
|
13
|
+
* Parsed FormData (formatted like the FormData object in a browser environment)
|
|
14
|
+
*/
|
|
15
|
+
export interface TextInput {
|
|
16
|
+
type: 'text'
|
|
17
|
+
name: string
|
|
18
|
+
data: string
|
|
19
|
+
}
|
|
20
|
+
export interface FileInput {
|
|
21
|
+
type: 'file'
|
|
22
|
+
name: string
|
|
23
|
+
filename: string
|
|
24
|
+
mimeType: string
|
|
25
|
+
data: Buffer
|
|
26
|
+
}
|
|
27
|
+
export type Input = TextInput | FileInput
|
|
28
|
+
|
|
29
|
+
export class FormData {
|
|
30
|
+
constructor(readonly inputs: Input[]) {}
|
|
31
|
+
|
|
32
|
+
get(name: string) {
|
|
33
|
+
return this.inputs.find(v => v.name === name)
|
|
34
|
+
}
|
|
35
|
+
getAll(name: string) {
|
|
36
|
+
return this.inputs.filter(v => v.name === name)
|
|
37
|
+
}
|
|
38
|
+
getText(name: string) {
|
|
39
|
+
return this.inputs.find((v): v is TextInput => v.name === name && v.type === 'text')?.data
|
|
40
|
+
}
|
|
41
|
+
getFile(name: string) {
|
|
42
|
+
return this.inputs.find((v): v is FileInput => v.name === name && v.type === 'file')
|
|
43
|
+
}
|
|
44
|
+
has(name: string) {
|
|
45
|
+
return !!this.get(name)
|
|
46
|
+
}
|
|
47
|
+
hasText(name: string) {
|
|
48
|
+
return this.getText(name) !== undefined
|
|
49
|
+
}
|
|
50
|
+
hasFile(name: string) {
|
|
51
|
+
return !!this.getFile(name)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 从 Content-Type Header 中解析出 form-data boundary
|
|
57
|
+
* Parse form-data boundary from Content-Type header.
|
|
58
|
+
*/
|
|
59
|
+
export function getBoundary(contentType: string) {
|
|
60
|
+
const prefix = 'multipart/form-data; boundary='
|
|
61
|
+
return contentType.startsWith(prefix) ? contentType.slice(prefix.length) : null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* parse form-data
|
|
66
|
+
*/
|
|
67
|
+
enum State {
|
|
68
|
+
Init = 0,
|
|
69
|
+
ReadingHeaders = 1,
|
|
70
|
+
ReadingData = 2,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseFormData(body: Buffer, boundary: string): FormData {
|
|
74
|
+
const inputs: Input[] = []
|
|
75
|
+
|
|
76
|
+
let rest = body
|
|
77
|
+
let state: State = State.Init
|
|
78
|
+
let partialInput: Input | undefined // 在 ReadingHeaders 状态里会为其赋值 Will be assigned when in the ReadingHeaders state
|
|
79
|
+
// eslint-disable-next-line no-constant-condition
|
|
80
|
+
while (true) {
|
|
81
|
+
if (!rest.byteLength) throw new HTTPError(400, 'form-data: not complete')
|
|
82
|
+
|
|
83
|
+
if (state === State.Init) {
|
|
84
|
+
const sep = `--${boundary}\r\n`
|
|
85
|
+
if (!startsWith(rest, sep)) throw new HTTPError(400, 'form-data: part separator invalid')
|
|
86
|
+
rest = rest.subarray(byteLength(sep))
|
|
87
|
+
state = State.ReadingHeaders
|
|
88
|
+
} else if (state === State.ReadingHeaders) {
|
|
89
|
+
if (!startsWith(rest, 'Content-Disposition: form-data;', true))
|
|
90
|
+
throw new HTTPError(400, 'form-data: headers or format invalid')
|
|
91
|
+
const headerEnds = Buffer.from('\r\n\r\n')
|
|
92
|
+
const headerMax = 10000 // header 部分最多允许这么多字节 Max allowd bytes of header
|
|
93
|
+
const headersEndIndex = rest.subarray(0, headerMax).indexOf(headerEnds)
|
|
94
|
+
if (headersEndIndex === -1) throw new HTTPError(400, 'form-data: headers invalid or too long')
|
|
95
|
+
|
|
96
|
+
const headers = rest
|
|
97
|
+
.subarray(0, headersEndIndex)
|
|
98
|
+
.toString()
|
|
99
|
+
.split('\r\n')
|
|
100
|
+
.map(item => {
|
|
101
|
+
const sepIndex = item.indexOf(':')
|
|
102
|
+
return sepIndex !== -1
|
|
103
|
+
? { name: item.slice(0, sepIndex).trim(), value: item.slice(sepIndex + 2).trim() }
|
|
104
|
+
: null
|
|
105
|
+
})
|
|
106
|
+
.filter((v): v is { name: string; value: string } => !!v)
|
|
107
|
+
|
|
108
|
+
const disposition =
|
|
109
|
+
headers.find(v => v.name.toLowerCase() === 'Content-Disposition'.toLowerCase())?.value ?? ''
|
|
110
|
+
const name = /(?:;| )name="(.*?)"(?:;|$)/.exec(disposition)?.[1] ?? ''
|
|
111
|
+
const filename = /(?:;| )filename="(.*?)"(?:;|$)/.exec(disposition)?.[1]
|
|
112
|
+
const contentType = headers.find(
|
|
113
|
+
v => v.name.toLowerCase() === 'Content-Type'.toLowerCase()
|
|
114
|
+
)?.value
|
|
115
|
+
partialInput =
|
|
116
|
+
filename === undefined
|
|
117
|
+
? { type: 'text', name, data: '' }
|
|
118
|
+
: { type: 'file', name, filename, mimeType: contentType ?? '', data: Buffer.from('') }
|
|
119
|
+
|
|
120
|
+
rest = rest.subarray(headersEndIndex + headerEnds.byteLength)
|
|
121
|
+
state = State.ReadingData
|
|
122
|
+
} else {
|
|
123
|
+
const dataEnds = Buffer.from(`\r\n--${boundary}`)
|
|
124
|
+
const dataEndIndex = rest.indexOf(dataEnds)
|
|
125
|
+
if (dataEndIndex === -1) throw new HTTPError(400, 'form-data: data no ends')
|
|
126
|
+
const data = rest.subarray(0, dataEndIndex)
|
|
127
|
+
const input = partialInput!
|
|
128
|
+
if (input.type === 'text') input.data = data.toString()
|
|
129
|
+
else input.data = data
|
|
130
|
+
inputs.push(input)
|
|
131
|
+
|
|
132
|
+
const afterData = rest.subarray(dataEndIndex + dataEnds.byteLength)
|
|
133
|
+
if (equals(afterData, '--') || equals(afterData, '--\r\n')) {
|
|
134
|
+
// 全部解析结束 Parse whole finished
|
|
135
|
+
break
|
|
136
|
+
} else if (startsWith(afterData, '\r\n')) {
|
|
137
|
+
// 开始解析下一部分 Parsing next part
|
|
138
|
+
rest = rest.subarray(dataEndIndex + byteLength('\r\n'))
|
|
139
|
+
state = State.Init
|
|
140
|
+
} else {
|
|
141
|
+
throw new HTTPError(400, 'form-data: invalid data ends')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return new FormData(inputs)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function byteLength(string: string) {
|
|
150
|
+
return Buffer.byteLength(string)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function startsWith(buffer: Buffer, string: string, caseInsensitive = false) {
|
|
154
|
+
const sliced = buffer.subarray(0, byteLength(string)).toString()
|
|
155
|
+
return caseInsensitive ? sliced.toLowerCase() === string.toLowerCase() : sliced === string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function equals(buffer: Buffer, string: string) {
|
|
159
|
+
return (
|
|
160
|
+
// 先进行长度比较,以避免 buffer 太大时进行 toString() 影响性能
|
|
161
|
+
// Perform a length comparison first to avoid the performance impact of toString() when the buffer is too large.
|
|
162
|
+
buffer.byteLength === byteLength(string) && buffer.toString() === string
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { HTTPError, type NodeRequest } from '../types.js'
|
|
2
|
+
import { getBoundary, parseFormData } from './form-data.js'
|
|
3
|
+
import { receiveBody } from './receive.js'
|
|
4
|
+
|
|
5
|
+
export interface BodyOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Maximum size of body, unit `M`.
|
|
8
|
+
* @default 1000
|
|
9
|
+
*/
|
|
10
|
+
limit?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Receive And Parse Request Body
|
|
15
|
+
* Support JSON And multipart/form-data
|
|
16
|
+
*/
|
|
17
|
+
export class RequestBody {
|
|
18
|
+
constructor(
|
|
19
|
+
readonly nodeRequest: NodeRequest,
|
|
20
|
+
readonly options: BodyOptions,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
get contentType() {
|
|
24
|
+
return this.nodeRequest.headers['content-type'] ?? ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected receiving?: Promise<Buffer | undefined>
|
|
28
|
+
async buffer() {
|
|
29
|
+
if (!this.receiving)
|
|
30
|
+
this.receiving = receiveBody(this.nodeRequest, (this.options.limit ?? 1000) * 1000 * 1000)
|
|
31
|
+
return this.receiving
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async json() {
|
|
35
|
+
if (this.contentType && !this.contentType.startsWith('application/json')) {
|
|
36
|
+
throw new HTTPError(400, "JSON parse failed, invalid 'Content-Type'.")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const buffer = await this.buffer()
|
|
40
|
+
if (!buffer) return undefined
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(buffer.toString()) as unknown
|
|
44
|
+
} catch (e) {
|
|
45
|
+
throw new HTTPError(400, 'Invalid JSON.')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async formData() {
|
|
50
|
+
const boundary = getBoundary(this.contentType)
|
|
51
|
+
if (boundary === null)
|
|
52
|
+
throw new HTTPError(400, "form-data parse failed, invalid 'Content-Type'.")
|
|
53
|
+
|
|
54
|
+
const buffer = await this.buffer()
|
|
55
|
+
if (!buffer) return undefined
|
|
56
|
+
|
|
57
|
+
return parseFormData(buffer, boundary)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type NodeRequest, HTTPError } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export async function receiveBody(
|
|
4
|
+
nodeRequest: NodeRequest,
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maximum size of body, unit `byte`
|
|
8
|
+
*/
|
|
9
|
+
limit: number,
|
|
10
|
+
) {
|
|
11
|
+
return new Promise<Buffer | undefined>(callback.bind(null, nodeRequest, limit))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function callback(
|
|
15
|
+
nodeRequest: NodeRequest,
|
|
16
|
+
limit: number,
|
|
17
|
+
resolve: (result: Buffer | undefined) => void,
|
|
18
|
+
reject: (error: HTTPError) => void,
|
|
19
|
+
) {
|
|
20
|
+
// 若客户端提供了 Content-Length,确认其小于限额(若不符合要求,直接跳过接收)
|
|
21
|
+
// If the client provides a Content-Length header, confirm that it is less than the maximum allowed size (if not, skip receiving it).
|
|
22
|
+
let contentLength: number | undefined
|
|
23
|
+
if ('content-length' in nodeRequest.headers) {
|
|
24
|
+
contentLength = parseInt(nodeRequest.headers['content-length'] ?? '', 10)
|
|
25
|
+
if (!isFinite(contentLength)) return void reject(new HTTPError(400, 'Invalid Content-Length'))
|
|
26
|
+
if (contentLength > limit) return void reject(new HTTPError(413))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const parts: Buffer[] = []
|
|
30
|
+
let recvLength = 0
|
|
31
|
+
const handleData = (part: Buffer) => {
|
|
32
|
+
parts.push(part)
|
|
33
|
+
recvLength += part.byteLength
|
|
34
|
+
|
|
35
|
+
if (recvLength > limit) {
|
|
36
|
+
nodeRequest.off('data', handleData)
|
|
37
|
+
nodeRequest.off('end', handleEnd)
|
|
38
|
+
reject(new HTTPError(413))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const handleEnd = () => {
|
|
42
|
+
if (contentLength !== undefined && recvLength !== contentLength)
|
|
43
|
+
reject(new HTTPError(400, 'Content-Length mismatch.'))
|
|
44
|
+
else resolve(parts.length ? Buffer.concat(parts) : undefined)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
nodeRequest.on('data', handleData)
|
|
48
|
+
nodeRequest.on('end', handleEnd)
|
|
49
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Status
|
|
3
|
+
* From:https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
|
|
4
|
+
*/
|
|
5
|
+
export const HTTPStatus = [
|
|
6
|
+
'100 Continue',
|
|
7
|
+
'101 Switching Protocols',
|
|
8
|
+
'103 Early Hints',
|
|
9
|
+
'200 OK',
|
|
10
|
+
'201 Created',
|
|
11
|
+
'202 Accepted',
|
|
12
|
+
'203 Non-Authoritative Information',
|
|
13
|
+
'204 No Content',
|
|
14
|
+
'205 Reset Content',
|
|
15
|
+
'206 Partial Content',
|
|
16
|
+
'300 Multiple Choices',
|
|
17
|
+
'301 Moved Permanently',
|
|
18
|
+
'302 Found',
|
|
19
|
+
'303 See Other',
|
|
20
|
+
'304 Not Modified',
|
|
21
|
+
'307 Temporary Redirect',
|
|
22
|
+
'308 Permanent Redirect',
|
|
23
|
+
'400 Bad Request',
|
|
24
|
+
'401 Unauthorized',
|
|
25
|
+
'402 Payment Required',
|
|
26
|
+
'403 Forbidden',
|
|
27
|
+
'404 Not Found',
|
|
28
|
+
'405 Method Not Allowed',
|
|
29
|
+
'406 Not Acceptable',
|
|
30
|
+
'407 Proxy Authentication Required',
|
|
31
|
+
'408 Request Timeout',
|
|
32
|
+
'409 Conflict',
|
|
33
|
+
'410 Gone',
|
|
34
|
+
'411 Length Required',
|
|
35
|
+
'412 Precondition Failed',
|
|
36
|
+
'413 Payload Too Large',
|
|
37
|
+
'414 URI Too Long',
|
|
38
|
+
'415 Unsupported Media Type',
|
|
39
|
+
'416 Range Not Satisfiable',
|
|
40
|
+
'417 Expectation Failed',
|
|
41
|
+
"418 I'm a teapot",
|
|
42
|
+
'422 Unprocessable Entity',
|
|
43
|
+
'425 Too Early',
|
|
44
|
+
'426 Upgrade Required',
|
|
45
|
+
'428 Precondition Required',
|
|
46
|
+
'429 Too Many Requests',
|
|
47
|
+
'431 Request Header Fields Too Large',
|
|
48
|
+
'451 Unavailable For Legal Reasons',
|
|
49
|
+
'500 Internal Server Error',
|
|
50
|
+
'501 Not Implemented',
|
|
51
|
+
'502 Bad Gateway',
|
|
52
|
+
'503 Service Unavailable',
|
|
53
|
+
'504 Gateway Timeout',
|
|
54
|
+
'505 HTTP Version Not Supported',
|
|
55
|
+
'506 Variant Also Negotiates',
|
|
56
|
+
'507 Insufficient Storage',
|
|
57
|
+
'508 Loop Detected',
|
|
58
|
+
'510 Not Extended',
|
|
59
|
+
'511 Network Authentication Required',
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Map(400 => '400 Bad Request')
|
|
64
|
+
*/
|
|
65
|
+
export const HTTPStatusMap = new Map(HTTPStatus.map(item => [parseInt(item.slice(0, 3), 10), item]))
|