Compare commits

..

8 Commits
v2 ... master

Author SHA1 Message Date
yutent 7f6e506f4a fixed UrlencodedParser 2024-08-12 11:30:54 +08:00
yutent a395c87101 加强版querystring 2024-07-16 17:45:52 +08:00
yutent 8afca9afaa 优化body解析 2024-07-10 16:44:17 +08:00
yutent 9d66a77b4d 增加hostname 2023-11-01 14:19:00 +08:00
yutent 2203b683f5 更新git地址 2023-10-31 18:55:50 +08:00
yutent face3185f7 2.0.2 2023-10-31 14:28:43 +08:00
yutent 5ecf94b5f3 优化content-type判断;增加protocol属性 2023-10-31 14:28:15 +08:00
yutent 53b0e2443e 更新readme 2023-10-30 17:34:06 +08:00
9 changed files with 172 additions and 174 deletions

110
Readme.md
View File

@ -1,4 +1,6 @@
![module info](https://nodei.co/npm/@gm5/request.png?downloads=true&downloadRank=true&stars=true)
![downloads](https://img.shields.io/npm/dt/@gm5/request.svg)
![version](https://img.shields.io/npm/v/@gm5/request.svg)
# @gm5/equest # @gm5/equest
> 对Http的request进一步封装, 提供常用的API. > 对Http的request进一步封装, 提供常用的API.
@ -19,115 +21,13 @@ http
.createServer((req, res) => { .createServer((req, res) => {
let request = new Request(req, res) let request = new Request(req, res)
console.log(request.origin) // {req, res}
// print the fixed url // print the fixed url
console.log(request.url) console.log(request.url)
request.ip() // get client ip address request.ip // get client ip address
// http://test.com/?foo=bar // http://test.com/?foo=bar
request.get('foo') // bar request.query['foo'] // bar
}) })
.listen(3000) .listen(3000)
``` ```
## API
### origin
> 返回原始的response & request对象
```js
console.log(request.origin) // {req: request, res: response}
```
### app
> 返回一级路由的名字
```js
// abc.com/foo/bar
console.log(request.app) // foo
```
### path
> 以数组形式,返回除一级路由之外剩下的路径
```js
// abc.com/foo/bar/aa/bb
console.log(request.path) // ['bar', 'aa', 'bb']
```
### url
> 返回修正过的url路径
```js
// abc.com/foo/bar/aa/bb
// abc.com////foo///bar/aa/bb
console.log(request.url) // foo/bar/aa/bb
```
### get([key[,xss]])
* key `<String>` 字段名 [可选], 不则返回全部参数
* xss `<Boolean>` 是否进行xss过滤 [可选], 默认为ture
> 返回URL上的query参数, 类似于`$_GET[]`;
```javascript
// http://test.com?name=foo&age=18
request.get('name') // foo
request.get('age') // 18
request.get() // {name: 'foo', age: 18}
request.get('weight') // return null if not exists
```
### post([key[,xss]])
* key `<String>` optional
* xss `<Boolean>` optional
> 读取post请求的body, 类似于 `$_POST[]`.
> **该方法返回的是Promise对象**
```javascript
// http://test.com
await request.post('name') // foo
await request.post('age') // 18
// return all if not yet argument given
await request.post() // {name: 'foo', age: 18}
await request.post('weight') // return null if not exists
```
### header([key])
* key `<String>` 字段名[可选], 不传则返回全部
> 返回请求头
```javascript
request.header('user-agent') // Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ...
// return all if not yet argument given
request.header() // {'user-agent': '...'[, ...]}
```
### ip()
> 获取客户端IP地址.
>
> It would return '127.0.0.1' maybe if in local area network.
### cookie(key)
> 获取客户端带上的cookie.
> 不传key时返回所有的

View File

@ -5,12 +5,13 @@
import 'es.shim' import 'es.shim'
import { fileURLToPath, parse } from 'node:url'
import { dirname, resolve } from 'node:path'
import fs from 'iofs'
import Parser from './lib/index.js' import Parser from './lib/index.js'
import { parseCookie } from './lib/cookie.js' import { parseCookie } from './lib/cookie.js'
import fs from 'iofs' import { querystring } from './lib/helper.js'
import { fileURLToPath, parse } from 'node:url'
import QS from 'node:querystring'
import { dirname, resolve } from 'node:path'
const DEFAULT_FORM_TYPE = 'application/x-www-form-urlencoded' const DEFAULT_FORM_TYPE = 'application/x-www-form-urlencoded'
@ -45,11 +46,14 @@ export default class Request {
#body = null #body = null
#cookies = Object.create(null) #cookies = Object.create(null)
controller = 'index'
method = 'GET' method = 'GET'
path = [] path = []
url = '' url = ''
host = '127.0.0.1' host = '127.0.0.1'
hostname = '127.0.0.1'
protocol = 'http'
constructor(req, res, opts = {}) { constructor(req, res, opts = {}) {
this.method = req.method.toUpperCase() this.method = req.method.toUpperCase()
@ -57,7 +61,10 @@ export default class Request {
this.#req = req this.#req = req
this.#res = res this.#res = res
this.host = req.headers['host'] this.host = req.headers['host'] || '127.0.0.1'
this.hostname = this.host.split(':')[0]
this.protocol = req.headers['x-forwarded-proto'] || 'http'
this.#cookies = parseCookie(this.headers['cookie'] || '') this.#cookies = parseCookie(this.headers['cookie'] || '')
Object.assign(this.#opts, opts) Object.assign(this.#opts, opts)
@ -67,44 +74,44 @@ export default class Request {
// 修正请求的url // 修正请求的url
#init() { #init() {
let _url = parse(this.#req.url) let url = parse(this.#req.url)
.pathname.slice(1) .pathname.slice(1)
.replace(/[\/]+$/, '') .replace(/[\/]+$/, '')
let app = '' // 将作为主控制器(即apps目录下的应用) let controller = '' // 将作为主控制器(即apps目录下的应用)
let pathArr = [] let path = []
// URL上不允许有非法字符 // URL上不允许有非法字符
if (/[^\w-/.,@~!$&:+'"=]/.test(decode(_url))) { if (/[^\w-/.,@~!$&:+'"=]/.test(decode(url))) {
this.#res.rendered = true this.#res.rendered = true
this.#res.writeHead(400, { this.#res.writeHead(400, {
'X-debug': `url [/${encode(_url)}] contains invalid characters` 'X-debug': `url [/${encode(url)}] contains invalid characters`
}) })
return this.#res.end(`Invalid characters: /${_url}`) return this.#res.end(`Invalid characters: /${url}`)
} }
// 修正url中可能出现的"多斜杠" // 修正url中可能出现的"多斜杠"
_url = _url.replace(/[\/]+/g, '/').replace(/^\//, '') url = url.replace(/[\/]+/g, '/').replace(/^\//, '')
pathArr = _url.split('/') path = url.split('/')
if (!pathArr[0] || pathArr[0] === '') { if (!path[0] || path[0] === '') {
pathArr[0] = 'index' path[0] = 'index'
} }
if (pathArr[0].indexOf('.') !== -1) { if (path[0].includes('.')) {
app = pathArr[0].slice(0, pathArr[0].indexOf('.')) controller = path[0].slice(0, path[0].indexOf('.'))
// 如果app为空(这种情况一般是url前面带了个"."造成的),则自动默认为index // 如果app为空(这种情况一般是url前面带了个"."造成的),则自动默认为index
if (!app || app === '') { if (!controller || controller === '') {
app = 'index' controller = 'index'
} }
} else { } else {
app = pathArr[0] controller = path[0]
} }
pathArr.shift() path.shift()
this.app = app this.controller = controller
this.url = _url this.url = url
this.path = pathArr this.path = path
} }
/** /**
@ -126,32 +133,22 @@ export default class Request {
this.#body = value this.#body = value
return return
} }
if (~contentType.indexOf('urlencoded')) {
if (
name.slice(0, 2) === '{"' &&
(name.slice(-2) === '"}' || value.slice(-2) === '"}')
) {
name = name.replace(/\s/g, '+')
if (value.slice(0, 1) === '=') value = '=' + value if (name.endsWith('[]')) {
return Object.assign(this.#body, JSON.parse(name + value))
}
}
if (name.slice(-2) === '[]') {
name = name.slice(0, -2) name = name.slice(0, -2)
if (typeof value === 'string') { if (typeof value === 'string') {
value = [value] value = [value]
} }
} else if (name.slice(-1) === ']') { } else if (name.slice(-1) === ']') {
let key = name.slice(name.lastIndexOf('[') + 1, -1) let idx = name.lastIndexOf('[')
name = name.slice(0, name.lastIndexOf('[')) let key = name.slice(idx + 1, -1)
name = name.slice(0, idx)
//多解析一层对象(也仅支持到这一层) //多解析一层对象(也仅支持到这一层)
if (name.slice(-1) === ']') { if (name.slice(-1) === ']') {
let pkey = name.slice(name.lastIndexOf('[') + 1, -1) idx = name.lastIndexOf('[')
name = name.slice(0, name.lastIndexOf('[')) let pkey = name.slice(idx + 1, -1)
name = name.slice(0, idx)
if (!this.#body.hasOwnProperty(name)) { if (!this.#body.hasOwnProperty(name)) {
this.#body[name] = {} this.#body[name] = {}
@ -191,6 +188,9 @@ export default class Request {
} }
} }
}) })
.on('buffer', buf => {
this.#body = buf
})
.on('error', out.reject) .on('error', out.reject)
.on('end', _ => { .on('end', _ => {
if (contentType.includes('urlencoded')) { if (contentType.includes('urlencoded')) {
@ -225,8 +225,8 @@ export default class Request {
get query() { get query() {
if (!this.#query) { if (!this.#query) {
let para = parse(this.#req.url).query let data = parse(this.#req.url).query
this.#query = QS.parse(para) this.#query = querystring(data)
} }
return this.#query return this.#query
} }

32
lib/helper.js Normal file
View File

@ -0,0 +1,32 @@
/**
* {}
* @author yutent<yutent.io@gmail.com>
* @date 2024/07/16 17:01:43
*/
import { parse } from 'node:querystring'
export function querystring(str) {
let query = parse(str)
for (let k of Object.keys(query)) {
let val = query[k]
if (k.endsWith('[]')) {
let _k = k.slice(0, -2)
query[_k] = val
delete query[k]
} else if (k.endsWith(']')) {
let idx = k.lastIndexOf('[')
let _pk = k.slice(0, idx)
let _k = k.slice(idx + 1, -1)
if (query[_pk]) {
query[_pk][_k] = val
} else {
query[_pk] = { [_k]: val }
}
delete query[k]
}
}
return query
}

View File

@ -6,7 +6,7 @@ import { EventEmitter } from 'node:events'
import File from './file.js' import File from './file.js'
import { MultipartParser } from './multipart_parser.js' import { MultipartParser } from './multipart_parser.js'
import { UrlencodedParser } from './urlencoded_parser.js' import { UrlencodedParser } from './urlencoded_parser.js'
import { OctetParser, EmptyParser } from './octet_parser.js' import { OctetParser, BufferParser, EmptyParser } from './octet_parser.js'
import { JSONParser } from './json_parser.js' import { JSONParser } from './json_parser.js'
function randomPath(uploadDir) { function randomPath(uploadDir) {
@ -98,8 +98,8 @@ export default class IncomingForm extends EventEmitter {
let value = Buffer.from('') let value = Buffer.from('')
part part
.on('data', buff => { .on('data', buf => {
value = Buffer.concat([value, buff]) value = Buffer.concat([value, buf])
}) })
.on('end', () => { .on('end', () => {
this.emit('field', part.name, value.toString(this.encoding)) this.emit('field', part.name, value.toString(this.encoding))
@ -138,7 +138,7 @@ export default class IncomingForm extends EventEmitter {
} }
#parseContentType() { #parseContentType() {
let contentType = this.headers['content-type'] let contentType = this.headers['content-type'] || ''
let lower = contentType.toLowerCase() let lower = contentType.toLowerCase()
if (this.bytesExpected === 0) { if (this.bytesExpected === 0) {
@ -167,7 +167,7 @@ export default class IncomingForm extends EventEmitter {
return this.#createJsonParser() return this.#createJsonParser()
} }
this.#handleError(new TypeError('unknown content-type: ' + contentType)) this.#createBufferParser()
} }
#parseContentLength() { #parseContentLength() {
@ -278,6 +278,10 @@ export default class IncomingForm extends EventEmitter {
#createUrlencodedParser() { #createUrlencodedParser() {
this.#parser = new UrlencodedParser() this.#parser = new UrlencodedParser()
if (this.bytesExpected) {
this.#parser.initLength(this.bytesExpected)
}
this.#parser this.#parser
.on('field', fields => this.emit('field', false, fields)) .on('field', fields => this.emit('field', false, fields))
.on('end', () => this.#handleEnd()) .on('end', () => this.#handleEnd())
@ -316,6 +320,21 @@ export default class IncomingForm extends EventEmitter {
.on('error', err => this.#handleError(err)) .on('error', err => this.#handleError(err))
} }
#createBufferParser() {
this.#parser = new BufferParser()
if (this.bytesExpected) {
this.#parser.initLength(this.bytesExpected)
}
this.#parser
.on('buffer', buf => {
this.emit('buffer', buf)
})
.on('end', () => this.#handleEnd())
.on('error', err => this.#handleError(err))
}
#clearUploads() { #clearUploads() {
while (this.#openedFiles.length) { while (this.#openedFiles.length) {
let file = this.#openedFiles.pop() let file = this.#openedFiles.pop()

View File

@ -1,7 +1,7 @@
import { EventEmitter } from 'node:events' import { EventEmitter } from 'node:events'
export class JSONParser extends EventEmitter { export class JSONParser extends EventEmitter {
#buff = Buffer.from('') #buf = Buffer.from('')
#byteLen = 0 #byteLen = 0
initLength(length) { initLength(length) {
@ -9,12 +9,12 @@ export class JSONParser extends EventEmitter {
} }
write(buffer) { write(buffer) {
this.#buff = Buffer.concat([this.#buff, buffer]) this.#buf = Buffer.concat([this.#buf, buffer])
} }
end() { end() {
if (this.#buff.length === this.#byteLen) { if (this.#buf.length === this.#byteLen) {
let data = this.#buff.toString() let data = this.#buf.toString()
let fields = data let fields = data
try { try {
fields = JSON.parse(data) fields = JSON.parse(data)
@ -28,14 +28,14 @@ export class JSONParser extends EventEmitter {
this.emit('field', false, fields) this.emit('field', false, fields)
this.emit('end') this.emit('end')
this.#buff = null this.#buf = null
} else { } else {
this.emit( this.emit(
'error', 'error',
new Error( new Error(
`The uploaded data is incomplete. Expected ${ `The uploaded data is incomplete. Expected ${
this.#byteLen this.#byteLen
}, Received ${this.#buff.length} .` }, Received ${this.#buf.length} .`
) )
) )
} }

View File

@ -73,11 +73,11 @@ export class MultipartParser {
this[k + 'Mark'] = v this[k + 'Mark'] = v
} }
#emit(name, buff, idx, cleanup) { #emit(name, buf, idx, cleanup) {
let mark = name + 'Mark' let mark = name + 'Mark'
if (this[mark] !== void 0) { if (this[mark] !== void 0) {
let start = this[mark] let start = this[mark]
let end = buff.length let end = buf.length
if (cleanup) { if (cleanup) {
end = idx end = idx
@ -89,7 +89,7 @@ export class MultipartParser {
if (start === end) { if (start === end) {
return return
} }
this['$' + name](buff.slice(start, end)) this['$' + name](buf.slice(start, end))
} }
} }

View File

@ -47,6 +47,36 @@ export class OctetParser extends EventEmitter {
} }
} }
export class BufferParser extends EventEmitter {
#buf = Buffer.from('')
#byteLen = 0
initLength(length) {
this.#byteLen = length
}
write(buffer) {
this.#buf = Buffer.concat([this.#buf, buffer])
}
end() {
if (this.#buf.length === this.#byteLen) {
this.emit('buffer', this.#buf)
this.emit('end')
this.#buf = null
} else {
this.emit(
'error',
new Error(
`The uploaded data is incomplete. Expected ${
this.#byteLen
}, Received ${this.#buf.length} .`
)
)
}
}
}
export class EmptyParser extends EventEmitter { export class EmptyParser extends EventEmitter {
write() {} write() {}

View File

@ -4,23 +4,40 @@
* @date 2023/10/27 12:14:05 * @date 2023/10/27 12:14:05
*/ */
import { parse } from 'node:querystring'
import { EventEmitter } from 'node:events' import { EventEmitter } from 'node:events'
import { querystring } from './helper.js'
export class UrlencodedParser extends EventEmitter { export class UrlencodedParser extends EventEmitter {
#buff = Buffer.from('') #buf = Buffer.from('')
#byteLen = 0
initLength(length) {
this.#byteLen = length
}
write(buffer) { write(buffer) {
this.#buff = Buffer.concat([this.#buff, buffer]) this.#buf = Buffer.concat([this.#buf, buffer])
} }
end() { end() {
let data = this.#buff.toString() if (this.#buf.length === this.#byteLen) {
let fields = parse(data) let data = this.#buf.toString()
let fields = querystring(data)
this.#buff = null this.#buf = null
this.emit('field', fields) this.emit('field', fields)
this.emit('end') this.emit('end')
this.#buf = null
} else {
this.emit(
'error',
new Error(
`The uploaded data is incomplete. Expected ${
this.#byteLen
}, Received ${this.#buf.length} .`
)
)
}
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@gm5/request", "name": "@gm5/request",
"version": "2.0.0", "version": "2.0.7",
"description": "对Http的Request进一步封装, 提供常用的API", "description": "对Http的Request进一步封装, 提供常用的API",
"main": "index.js", "main": "index.js",
"author": "yutent", "author": "yutent",
@ -15,9 +15,9 @@
"http" "http"
], ],
"dependencies": { "dependencies": {
"es.shim": "^2.0.1", "es.shim": "^2.2.0",
"iofs": "^1.5.0" "iofs": "^1.5.3"
}, },
"repository": "https://github.com/bytedo/gmf.request.git", "repository": "https://git.wkit.fun/gm5/request.git",
"license": "MIT" "license": "MIT"
} }