Compare commits

..

No commits in common. "master" and "v2" have entirely different histories.
master ... v2

9 changed files with 174 additions and 172 deletions

110
Readme.md
View File

@ -1,6 +1,4 @@
![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.
@ -21,13 +19,115 @@ 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.query['foo'] // bar request.get('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,13 +5,12 @@
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 { querystring } from './lib/helper.js' import fs from 'iofs'
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'
@ -46,14 +45,11 @@ 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()
@ -61,10 +57,7 @@ export default class Request {
this.#req = req this.#req = req
this.#res = res this.#res = res
this.host = req.headers['host'] || '127.0.0.1' this.host = req.headers['host']
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)
@ -74,44 +67,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 controller = '' // 将作为主控制器(即apps目录下的应用) let app = '' // 将作为主控制器(即apps目录下的应用)
let path = [] let pathArr = []
// 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(/^\//, '')
path = url.split('/') pathArr = _url.split('/')
if (!path[0] || path[0] === '') { if (!pathArr[0] || pathArr[0] === '') {
path[0] = 'index' pathArr[0] = 'index'
} }
if (path[0].includes('.')) { if (pathArr[0].indexOf('.') !== -1) {
controller = path[0].slice(0, path[0].indexOf('.')) app = pathArr[0].slice(0, pathArr[0].indexOf('.'))
// 如果app为空(这种情况一般是url前面带了个"."造成的),则自动默认为index // 如果app为空(这种情况一般是url前面带了个"."造成的),则自动默认为index
if (!controller || controller === '') { if (!app || app === '') {
controller = 'index' app = 'index'
} }
} else { } else {
controller = path[0] app = pathArr[0]
} }
path.shift() pathArr.shift()
this.controller = controller this.app = app
this.url = url this.url = _url
this.path = path this.path = pathArr
} }
/** /**
@ -133,22 +126,32 @@ 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 (name.endsWith('[]')) { if (value.slice(0, 1) === '=') value = '=' + value
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 idx = name.lastIndexOf('[') let key = name.slice(name.lastIndexOf('[') + 1, -1)
let key = name.slice(idx + 1, -1) name = name.slice(0, name.lastIndexOf('['))
name = name.slice(0, idx)
//多解析一层对象(也仅支持到这一层) //多解析一层对象(也仅支持到这一层)
if (name.slice(-1) === ']') { if (name.slice(-1) === ']') {
idx = name.lastIndexOf('[') let pkey = name.slice(name.lastIndexOf('[') + 1, -1)
let pkey = name.slice(idx + 1, -1) name = name.slice(0, name.lastIndexOf('['))
name = name.slice(0, idx)
if (!this.#body.hasOwnProperty(name)) { if (!this.#body.hasOwnProperty(name)) {
this.#body[name] = {} this.#body[name] = {}
@ -188,9 +191,6 @@ 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 data = parse(this.#req.url).query let para = parse(this.#req.url).query
this.#query = querystring(data) this.#query = QS.parse(para)
} }
return this.#query return this.#query
} }

View File

@ -1,32 +0,0 @@
/**
* {}
* @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, BufferParser, EmptyParser } from './octet_parser.js' import { OctetParser, 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', buf => { .on('data', buff => {
value = Buffer.concat([value, buf]) value = Buffer.concat([value, buff])
}) })
.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.#createBufferParser() this.#handleError(new TypeError('unknown content-type: ' + contentType))
} }
#parseContentLength() { #parseContentLength() {
@ -278,10 +278,6 @@ 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())
@ -320,21 +316,6 @@ 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 {
#buf = Buffer.from('') #buff = Buffer.from('')
#byteLen = 0 #byteLen = 0
initLength(length) { initLength(length) {
@ -9,12 +9,12 @@ export class JSONParser extends EventEmitter {
} }
write(buffer) { write(buffer) {
this.#buf = Buffer.concat([this.#buf, buffer]) this.#buff = Buffer.concat([this.#buff, buffer])
} }
end() { end() {
if (this.#buf.length === this.#byteLen) { if (this.#buff.length === this.#byteLen) {
let data = this.#buf.toString() let data = this.#buff.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.#buf = null this.#buff = 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.#buf.length} .` }, Received ${this.#buff.length} .`
) )
) )
} }

View File

@ -73,11 +73,11 @@ export class MultipartParser {
this[k + 'Mark'] = v this[k + 'Mark'] = v
} }
#emit(name, buf, idx, cleanup) { #emit(name, buff, 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 = buf.length let end = buff.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](buf.slice(start, end)) this['$' + name](buff.slice(start, end))
} }
} }

View File

@ -47,36 +47,6 @@ 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,40 +4,23 @@
* @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 {
#buf = Buffer.from('') #buff = Buffer.from('')
#byteLen = 0
initLength(length) {
this.#byteLen = length
}
write(buffer) { write(buffer) {
this.#buf = Buffer.concat([this.#buf, buffer]) this.#buff = Buffer.concat([this.#buff, buffer])
} }
end() { end() {
if (this.#buf.length === this.#byteLen) { let data = this.#buff.toString()
let data = this.#buf.toString() let fields = parse(data)
let fields = querystring(data)
this.#buf = null this.#buff = 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.7", "version": "2.0.0",
"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.2.0", "es.shim": "^2.0.1",
"iofs": "^1.5.3" "iofs": "^1.5.0"
}, },
"repository": "https://git.wkit.fun/gm5/request.git", "repository": "https://github.com/bytedo/gmf.request.git",
"license": "MIT" "license": "MIT"
} }