宇天 2020-09-16 20:07:28 +08:00
commit 67e5da5098
11 changed files with 1507 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.Spotlight-V100
.Trashes
.DS_Store
.AppleDouble
.LSOverride
._*
.idea
.vscode

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

136
Readme.md Normal file
View File

@ -0,0 +1,136 @@
![module info](https://nodei.co/npm/http.request.png?downloads=true&downloadRank=true&stars=true)
# http.request
> `http.request` is a module that let you can easily using on http server.
## Install
```bash
npm i http.request
```
## Usage
```javascript
let Request = require('http.request')
let http = require('http')
http
.createServer((req, res) => {
let request = new Request(req, res)
console.log(request.origin) // {req, res}
// print the fixed url
console.log(request.url)
request.ip() // get client ip address
// http://test.com/?foo=bar
request.get('foo') // bar
})
.listen(3000)
```
## API
### origin
> return the origin request object and response object.
```js
console.log(request.origin) // {req: request, res: response}
```
### app
> return this first part of url
```js
// abc.com/foo/bar
console.log(request.app) // foo
```
### path
> return this extra part of url
```js
// abc.com/foo/bar/aa/bb
console.log(request.path) // ['bar', 'aa', 'bb']
```
### url
> return this fixed url
```js
// abc.com/foo/bar/aa/bb
// abc.com////foo///bar/aa/bb
console.log(request.url) // foo/bar/aa/bb
```
### router
> return this router params
```js
// abc.com/foo/bar/aa/bb/xx/yy
console.log(request.router) // {aa: 'bb', xx: 'yy'}
```
### get([key[,xss]])
* key `<String>` optional
* xss `<Boolean>` optional
> Get the fieldset from url. Just like PHP's `$_GET[]`;
> If `xss` is set to be true, the result will be filtered out with base xss.
```javascript
// http://test.com?name=foo&age=18
request.get('name') // foo
request.get('age') // 18
// return all if not yet argument given
request.get() // {name: 'foo', age: 18}
request.get('weight') // return null if not exists
```
### post([key[,xss]])
* key `<String>` optional
* xss `<Boolean>` optional
> Get the http body content, just like PHP's `$_POST[]`.
>
> **this function must use await/yiled command**
```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>` optional
> return http headers.
```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()
> return the client IP address.
>
> It would return '127.0.0.1' maybe if in local area network.

271
index.js Normal file
View File

@ -0,0 +1,271 @@
/**
* @author yutent<yutent.io@gmail.com>
* @date 2020/09/16 16:05:57
*/
import 'es.shim'
import Parser from './lib/index.js'
import fs from 'iofs'
import URL from 'url'
import QS from 'querystring'
const tmpdir = process.cwd() + '/.tmp/'
function hideProperty(host, name, value) {
Object.defineProperty(host, name, {
value: value,
writable: true,
enumerable: false,
configurable: true
})
}
export default class Request {
constructor(req, res) {
this.method = req.method.toUpperCase()
this.params = {}
hideProperty(this, 'origin', { req, res })
hideProperty(this, '__GET__', null)
hideProperty(this, '__POST__', null)
this.__fixUrl()
if (!fs.isdir(tmpdir)) {
fs.mkdir(tmpdir)
} else {
// 清除2个小时前的所有临时文件
let list = fs.ls(tmpdir)
list.forEach(it => {
if (fs.stat(it).atime < Date.now() - 2 * 3600 * 1000) {
fs.rm(it)
}
})
}
}
// 修正请求的url
__fixUrl() {
let _url = URL.parse(this.origin.req.url)
.pathname.slice(1)
.replace(/[\/]+$/, '')
let app = '' // 将作为主控制器(即apps目录下的应用)
let pathArr = []
let tmpArr = []
// URL上不允许有非法字符
if (/[^\w\-\/\.]/.test(_url)) {
this.origin.res.rendered = true
this.origin.res.writeHead(400, {
'X-debug': 'url[' + _url + '] contains illegal characters'
})
return this.origin.res.end('')
}
// 修正url中可能出现的"多斜杠"
_url = _url.replace(/[\/]+/g, '/').replace(/^\//, '')
pathArr = _url.split('/')
if (!pathArr[0] || pathArr[0] === '') {
pathArr[0] = 'index'
}
if (pathArr[0].indexOf('.') !== -1) {
app = pathArr[0].slice(0, pathArr[0].indexOf('.'))
// 如果app为空(这种情况一般是url前面带了个"."造成的),则自动默认为index
if (!app || app === '') {
app = 'index'
}
} else {
app = pathArr[0]
}
pathArr.shift()
// 将path第3段之后的部分, 每2个一组转为key-val数据对象, 存入params中
tmpArr = pathArr.slice(1).concat()
while (tmpArr.length) {
this.params[tmpArr.shift()] = tmpArr.shift() || null
}
tmpArr = undefined
for (let i in this.params) {
if (!this.params[i]) {
continue
}
// 修正数字类型,把符合条件的数字字符串转为数字(也许会误转, 但总的来说是利大弊)
this.params[i] = Number.parse(this.params[i])
}
this.app = app
this.url = _url
this.path = pathArr
}
/**
* [get 同php的$_GET]
*/
get(key = '', xss = true) {
xss = !!xss
if (!this.__GET__) {
let para = URL.parse(this.origin.req.url).query
para = Object.assign({}, QS.parse(para))
if (xss) {
for (let i in para) {
if (!para[i]) {
continue
}
if (Array.isArray(para[i])) {
para[i] = para[i].map(it => {
it = Number.parse(it.trim().xss())
return it
})
} else {
para[i] = Number.parse(para[i].trim().xss())
}
}
}
this.__GET__ = para
}
return key
? this.__GET__.hasOwnProperty(key)
? this.__GET__[key]
: null
: this.__GET__
}
/**
* [post 接收post, 需要 await ]
* @param {Str} key [字段]
*/
post(key = '', xss = true) {
let para = {}
let out = Promise.defer()
xss = !!xss
//如果之前已经缓存过,则直接从缓存读取
if (this.__POST__) {
if (key) {
return this.__POST__.hasOwnProperty(key) ? this.__POST__[key] : null
} else {
return this.__POST__
}
}
let form = new Parser()
form.uploadDir = tmpdir
form.parse(this.origin.req)
form.on('field', (name, value) => {
if (name === false) {
para = value
return
}
if (~this.header('content-type').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
return Object.assign(para, JSON.parse(name + value))
}
}
if (typeof value === 'string') {
value = xss ? value.xss() : value
}
if (name.slice(-2) === '[]') {
name = name.slice(0, -2)
if (typeof value === 'string') {
value = [value]
}
} else if (name.slice(-1) === ']') {
let key = name.slice(name.lastIndexOf('[') + 1, -1)
name = name.slice(0, name.lastIndexOf('['))
//多解析一层对象(也仅支持到这一层)
if (name.slice(-1) === ']') {
let pkey = name.slice(name.lastIndexOf('[') + 1, -1)
name = name.slice(0, name.lastIndexOf('['))
if (!para.hasOwnProperty(name)) {
para[name] = {}
}
if (!para[name].hasOwnProperty(pkey)) {
para[name][pkey] = {}
}
para[name][pkey][key] = value
} else {
if (!para.hasOwnProperty(name)) {
para[name] = {}
}
para[name][key] = value
}
return
}
para[name] = value
})
form.on('file', (name, file) => {
if (name.slice(-2) === '[]') {
name = name.slice(0, -2)
}
if (!para.hasOwnProperty(name)) {
para[name] = file
} else {
if (!Array.isArray(para[name])) {
para[name] = [para[name]]
}
para[name].push(file)
}
})
form.on('error', out.reject)
form.on('end', err => {
if (~this.header('content-type').indexOf('urlencoded')) {
for (let i in para) {
if (typeof para[i] === 'string') {
if (!para[i]) {
continue
}
para[i] = Number.parse(para[i])
}
}
}
this._postParam = para
if (key) {
return out.resolve(para.hasOwnProperty(key) ? para[key] : null)
} else {
return out.resolve(para)
}
})
return out.promise
}
//获取响应头
header(key = '') {
key = key ? (key + '').toLowerCase() : null
return !!key ? this.origin.req.headers[key] : this.origin.req.headers
}
//获取客户端IP
ip() {
return (
this.header('x-real-ip') ||
this.header('x-forwarded-for') ||
this.origin.req.connection.remoteAddress.replace('::ffff:', '')
)
}
}

70
lib/file.js Normal file
View File

@ -0,0 +1,70 @@
import util from 'util'
import { WriteStream } from 'fs'
import { EventEmitter } from 'events'
import crypto from 'crypto'
export default function File(properties) {
EventEmitter.call(this)
this.size = 0
this.path = null
this.name = null
this.type = null
this.hash = null
this.lastModifiedDate = null
this._writeStream = null
for (var key in properties) {
this[key] = properties[key]
}
if (typeof this.hash === 'string') {
this.hash = crypto.createHash(properties.hash)
} else {
this.hash = null
}
}
util.inherits(File, EventEmitter)
File.prototype.open = function() {
this._writeStream = new WriteStream(this.path)
}
File.prototype.toJSON = function() {
return {
size: this.size,
path: this.path,
name: this.name,
type: this.type,
mtime: this.lastModifiedDate,
length: this.length,
filename: this.filename,
mime: this.mime
}
}
File.prototype.write = function(buffer, cb) {
var self = this
if (self.hash) {
self.hash.update(buffer)
}
this._writeStream.write(buffer, function() {
self.lastModifiedDate = new Date()
self.size += buffer.length
self.emit('progress', self.size)
cb()
})
}
File.prototype.end = function(cb) {
var self = this
if (self.hash) {
self.hash = self.hash.digest('hex')
}
this._writeStream.end(function() {
self.emit('end')
cb()
})
}

571
lib/index.js Normal file
View File

@ -0,0 +1,571 @@
import crypto from 'crypto'
import fs from 'fs'
import util from 'util'
import path from 'path'
import File from './file.js'
import { EventEmitter } from 'events'
import { Stream } from 'stream'
import { StringDecoder } from 'string_decoder'
import { MultipartParser } from './multipart_parser.js'
import { QuerystringParser } from './querystring_parser.js'
import { OctetParser } from './octet_parser.js'
import { JSONParser } from './json_parser.js'
export default function IncomingForm(opts) {
EventEmitter.call(this)
opts = opts || {}
this.error = null
this.ended = false
this.maxFields = opts.maxFields || 1000
this.maxFieldsSize = opts.maxFieldsSize || 2 * 1024 * 1024
this.keepExtensions = opts.keepExtensions || false
this.uploadDir = opts.uploadDir
this.encoding = opts.encoding || 'utf-8'
this.headers = null
this.type = null
this.hash = opts.hash || false
this.multiples = opts.multiples || false
this.bytesReceived = null
this.bytesExpected = null
this._parser = null
this._flushing = 0
this._fieldsSize = 0
this.openedFiles = []
}
util.inherits(IncomingForm, EventEmitter)
IncomingForm.prototype.parse = function(req, cb) {
this.pause = function() {
try {
req.pause()
} catch (err) {
// the stream was destroyed
if (!this.ended) {
// before it was completed, crash & burn
this._error(err)
}
return false
}
return true
}
this.resume = function() {
try {
req.resume()
} catch (err) {
// the stream was destroyed
if (!this.ended) {
// before it was completed, crash & burn
this._error(err)
}
return false
}
return true
}
// Setup callback first, so we don't miss anything from data events emitted
// immediately.
if (cb) {
var fields = {},
files = {}
this.on('field', function(name, value) {
fields[name] = value
})
.on('file', function(name, file) {
if (this.multiples) {
if (files[name]) {
if (!Array.isArray(files[name])) {
files[name] = [files[name]]
}
files[name].push(file)
} else {
files[name] = file
}
} else {
files[name] = file
}
})
.on('error', function(err) {
cb(err, fields, files)
})
.on('end', function() {
cb(null, fields, files)
})
}
// Parse headers and setup the parser, ready to start listening for data.
this.writeHeaders(req.headers)
// Start listening for data.
var self = this
req
.on('error', function(err) {
self._error(err)
})
.on('aborted', function() {
self.emit('aborted')
self._error(new Error('Request aborted'))
})
.on('data', function(buffer) {
self.write(buffer)
})
.on('end', function() {
if (self.error) {
return
}
var err = self._parser.end()
if (err) {
self._error(err)
}
})
return this
}
IncomingForm.prototype.writeHeaders = function(headers) {
this.headers = headers
this._parseContentLength()
this._parseContentType()
}
IncomingForm.prototype.write = function(buffer) {
if (this.error) {
return
}
if (!this._parser) {
this._error(new Error('uninitialized parser'))
return
}
this.bytesReceived += buffer.length
this.emit('progress', this.bytesReceived, this.bytesExpected)
var bytesParsed = this._parser.write(buffer)
if (bytesParsed !== buffer.length) {
this._error(
new Error(
'parser error, ' +
bytesParsed +
' of ' +
buffer.length +
' bytes parsed'
)
)
}
return bytesParsed
}
IncomingForm.prototype.pause = function() {
// this does nothing, unless overwritten in IncomingForm.parse
return false
}
IncomingForm.prototype.resume = function() {
// this does nothing, unless overwritten in IncomingForm.parse
return false
}
IncomingForm.prototype.onPart = function(part) {
// this method can be overwritten by the user
this.handlePart(part)
}
IncomingForm.prototype.handlePart = function(part) {
var self = this
if (part.filename === undefined) {
var value = '',
decoder = new StringDecoder(this.encoding)
part.on('data', function(buffer) {
self._fieldsSize += buffer.length
if (self._fieldsSize > self.maxFieldsSize) {
self._error(
new Error(
'maxFieldsSize exceeded, received ' +
self._fieldsSize +
' bytes of field data'
)
)
return
}
value += decoder.write(buffer)
})
part.on('end', function() {
self.emit('field', part.name, value)
})
return
}
this._flushing++
var file = new File({
path: this._uploadPath(part.filename),
name: part.filename,
type: part.mime,
hash: self.hash
})
this.emit('fileBegin', part.name, file)
file.open()
this.openedFiles.push(file)
part.on('data', function(buffer) {
if (buffer.length == 0) {
return
}
self.pause()
file.write(buffer, function() {
self.resume()
})
})
part.on('end', function() {
file.end(function() {
self._flushing--
self.emit('file', part.name, file)
self._maybeEnd()
})
})
}
function dummyParser(self) {
return {
end: function() {
self.ended = true
self._maybeEnd()
return null
}
}
}
IncomingForm.prototype._parseContentType = function() {
if (this.bytesExpected === 0) {
this._parser = dummyParser(this)
return
}
if (!this.headers['content-type']) {
this._error(new Error('bad content-type header, no content-type'))
return
}
if (this.headers['content-type'].match(/octet-stream/i)) {
this._initOctetStream()
return
}
if (this.headers['content-type'].match(/urlencoded/i)) {
this._initUrlencoded()
return
}
if (this.headers['content-type'].match(/multipart/i)) {
var m = this.headers['content-type'].match(
/boundary=(?:"([^"]+)"|([^;]+))/i
)
if (m) {
this._initMultipart(m[1] || m[2])
} else {
this._error(new Error('bad content-type header, no multipart boundary'))
}
return
}
if (this.headers['content-type'].match(/json|appliation|plain|text/i)) {
this._initJSONencoded()
return
}
this._error(
new Error(
'bad content-type header, unknown content-type: ' +
this.headers['content-type']
)
)
}
IncomingForm.prototype._error = function(err) {
if (this.error || this.ended) {
return
}
this.error = err
this.emit('error', err)
if (Array.isArray(this.openedFiles)) {
this.openedFiles.forEach(function(file) {
file._writeStream.destroy()
setTimeout(fs.unlink, 0, file.path, function(error) {})
})
}
}
IncomingForm.prototype._parseContentLength = function() {
this.bytesReceived = 0
if (this.headers['content-length']) {
this.bytesExpected = parseInt(this.headers['content-length'], 10)
} else if (this.headers['transfer-encoding'] === undefined) {
this.bytesExpected = 0
}
if (this.bytesExpected !== null) {
this.emit('progress', this.bytesReceived, this.bytesExpected)
}
}
IncomingForm.prototype._newParser = function() {
return new MultipartParser()
}
IncomingForm.prototype._initMultipart = function(boundary) {
this.type = 'multipart'
var parser = new MultipartParser(),
self = this,
headerField,
headerValue,
part
parser.initWithBoundary(boundary)
parser.onPartBegin = function() {
part = new Stream()
part.readable = true
part.headers = {}
part.name = null
part.filename = null
part.mime = null
part.transferEncoding = 'binary'
part.transferBuffer = ''
headerField = ''
headerValue = ''
}
parser.onHeaderField = function(b, start, end) {
headerField += b.toString(self.encoding, start, end)
}
parser.onHeaderValue = function(b, start, end) {
headerValue += b.toString(self.encoding, start, end)
}
parser.onHeaderEnd = function() {
headerField = headerField.toLowerCase()
part.headers[headerField] = headerValue
var m = headerValue.match(/\bname="([^"]+)"/i)
if (headerField == 'content-disposition') {
if (m) {
part.name = m[1]
}
part.filename = self._fileName(headerValue)
} else if (headerField == 'content-type') {
part.mime = headerValue
} else if (headerField == 'content-transfer-encoding') {
part.transferEncoding = headerValue.toLowerCase()
}
headerField = ''
headerValue = ''
}
parser.onHeadersEnd = function() {
switch (part.transferEncoding) {
case 'binary':
case '7bit':
case '8bit':
parser.onPartData = function(b, start, end) {
part.emit('data', b.slice(start, end))
}
parser.onPartEnd = function() {
part.emit('end')
}
break
case 'base64':
parser.onPartData = function(b, start, end) {
part.transferBuffer += b.slice(start, end).toString('ascii')
/*
four bytes (chars) in base64 converts to three bytes in binary
encoding. So we should always work with a number of bytes that
can be divided by 4, it will result in a number of buytes that
can be divided vy 3.
*/
var offset = parseInt(part.transferBuffer.length / 4, 10) * 4
part.emit(
'data',
Buffer.from(part.transferBuffer.substring(0, offset), 'base64')
)
part.transferBuffer = part.transferBuffer.substring(offset)
}
parser.onPartEnd = function() {
part.emit('data', Buffer.from(part.transferBuffer, 'base64'))
part.emit('end')
}
break
default:
return self._error(new Error('unknown transfer-encoding'))
}
self.onPart(part)
}
parser.onEnd = function() {
self.ended = true
self._maybeEnd()
}
this._parser = parser
}
IncomingForm.prototype._fileName = function(headerValue) {
var m = headerValue.match(/\bfilename="(.*?)"($|; )/i)
if (!m) return
var filename = m[1].substr(m[1].lastIndexOf('\\') + 1)
filename = filename.replace(/%22/g, '"')
filename = filename.replace(/&#([\d]{4});/g, function(m, code) {
return String.fromCharCode(code)
})
return filename
}
IncomingForm.prototype._initUrlencoded = function() {
this.type = 'urlencoded'
var parser = new QuerystringParser(this.maxFields)
parser.onField = (key, val) => {
this.emit('field', key, val)
}
parser.onEnd = () => {
this.ended = true
this._maybeEnd()
}
this._parser = parser
}
IncomingForm.prototype._initOctetStream = function() {
this.type = 'octet-stream'
var filename = this.headers['x-file-name']
var mime = this.headers['content-type']
var file = new File({
path: this._uploadPath(filename),
name: filename,
type: mime
})
this.emit('fileBegin', filename, file)
file.open()
this._flushing++
var self = this
self._parser = new OctetParser()
//Keep track of writes that haven't finished so we don't emit the file before it's done being written
var outstandingWrites = 0
self._parser.on('data', function(buffer) {
self.pause()
outstandingWrites++
file.write(buffer, function() {
outstandingWrites--
self.resume()
if (self.ended) {
self._parser.emit('doneWritingFile')
}
})
})
self._parser.on('end', function() {
self._flushing--
self.ended = true
var done = function() {
file.end(function() {
self.emit('file', 'file', file)
self._maybeEnd()
})
}
if (outstandingWrites === 0) {
done()
} else {
self._parser.once('doneWritingFile', done)
}
})
}
IncomingForm.prototype._initJSONencoded = function() {
this.type = 'json'
var parser = new JSONParser(),
self = this
if (this.bytesExpected) {
parser.initWithLength(this.bytesExpected)
}
parser.onField = function(key, val) {
self.emit('field', key, val)
}
parser.onEnd = function() {
self.ended = true
self._maybeEnd()
}
this._parser = parser
}
IncomingForm.prototype._uploadPath = function(filename) {
var name = 'upload_'
var buf = crypto.randomBytes(16)
for (var i = 0; i < buf.length; ++i) {
name += ('0' + buf[i].toString(16)).slice(-2)
}
if (this.keepExtensions) {
var ext = path.extname(filename)
ext = ext.replace(/(\.[a-z0-9]+).*/i, '$1')
name += ext
}
return path.join(this.uploadDir, name)
}
IncomingForm.prototype._maybeEnd = function() {
if (!this.ended || this._flushing || this.error) {
return
}
this.emit('end')
}

43
lib/json_parser.js Normal file
View File

@ -0,0 +1,43 @@
export function JSONParser() {
this.data = Buffer.from('')
this.bytesWritten = 0
}
JSONParser.prototype.initWithLength = function(length) {
this.data = Buffer.alloc(length)
}
JSONParser.prototype.write = function(buffer) {
if (this.data.length >= this.bytesWritten + buffer.length) {
buffer.copy(this.data, this.bytesWritten)
} else {
this.data = Buffer.concat([this.data, buffer])
}
this.bytesWritten += buffer.length
return buffer.length
}
JSONParser.prototype.end = function() {
var data = this.data.toString('utf8')
var fields
try {
fields = JSON.parse(data)
} catch (e) {
fields = Function(`try{return ${data}}catch(e){}`)() || data
}
if (typeof fields === 'object') {
if (Array.isArray(fields)) {
this.onField(false, fields)
} else {
for (let field in fields) {
this.onField(field, fields[field])
}
}
} else {
this.onField(false, fields)
}
this.data = null
this.onEnd()
}

327
lib/multipart_parser.js Normal file
View File

@ -0,0 +1,327 @@
var s = 0,
S = {
PARSER_UNINITIALIZED: s++,
START: s++,
START_BOUNDARY: s++,
HEADER_FIELD_START: s++,
HEADER_FIELD: s++,
HEADER_VALUE_START: s++,
HEADER_VALUE: s++,
HEADER_VALUE_ALMOST_DONE: s++,
HEADERS_ALMOST_DONE: s++,
PART_DATA_START: s++,
PART_DATA: s++,
PART_END: s++,
END: s++
},
f = 1,
F = {
PART_BOUNDARY: f,
LAST_BOUNDARY: (f *= 2)
},
LF = 10,
CR = 13,
SPACE = 32,
HYPHEN = 45,
COLON = 58,
A = 97,
Z = 122,
lower = function(c) {
return c | 0x20
}
export function MultipartParser() {
this.boundary = null
this.boundaryChars = null
this.lookbehind = null
this.state = S.PARSER_UNINITIALIZED
this.index = null
this.flags = 0
}
MultipartParser.stateToString = function(stateNumber) {
for (var state in S) {
var number = S[state]
if (number === stateNumber) return state
}
}
MultipartParser.prototype.initWithBoundary = function(str) {
this.boundary = Buffer.alloc(str.length + 4)
this.boundary.write('\r\n--', 0)
this.boundary.write(str, 4)
this.lookbehind = Buffer.alloc(this.boundary.length + 8)
this.state = S.START
this.boundaryChars = {}
for (var i = 0; i < this.boundary.length; i++) {
this.boundaryChars[this.boundary[i]] = true
}
}
MultipartParser.prototype.write = function(buffer) {
var self = this,
i = 0,
len = buffer.length,
prevIndex = this.index,
index = this.index,
state = this.state,
flags = this.flags,
lookbehind = this.lookbehind,
boundary = this.boundary,
boundaryChars = this.boundaryChars,
boundaryLength = this.boundary.length,
boundaryEnd = boundaryLength - 1,
bufferLength = buffer.length,
c,
cl,
mark = function(name) {
self[name + 'Mark'] = i
},
clear = function(name) {
delete self[name + 'Mark']
},
callback = function(name, buffer, start, end) {
if (start !== undefined && start === end) {
return
}
var callbackSymbol =
'on' + name.substr(0, 1).toUpperCase() + name.substr(1)
if (callbackSymbol in self) {
self[callbackSymbol](buffer, start, end)
}
},
dataCallback = function(name, clear) {
var markSymbol = name + 'Mark'
if (!(markSymbol in self)) {
return
}
if (!clear) {
callback(name, buffer, self[markSymbol], buffer.length)
self[markSymbol] = 0
} else {
callback(name, buffer, self[markSymbol], i)
delete self[markSymbol]
}
}
for (i = 0; i < len; i++) {
c = buffer[i]
switch (state) {
case S.PARSER_UNINITIALIZED:
return i
case S.START:
index = 0
state = S.START_BOUNDARY
case S.START_BOUNDARY:
if (index == boundary.length - 2) {
if (c == HYPHEN) {
flags |= F.LAST_BOUNDARY
} else if (c != CR) {
return i
}
index++
break
} else if (index - 1 == boundary.length - 2) {
if (flags & F.LAST_BOUNDARY && c == HYPHEN) {
callback('end')
state = S.END
flags = 0
} else if (!(flags & F.LAST_BOUNDARY) && c == LF) {
index = 0
callback('partBegin')
state = S.HEADER_FIELD_START
} else {
return i
}
break
}
if (c != boundary[index + 2]) {
index = -2
}
if (c == boundary[index + 2]) {
index++
}
break
case S.HEADER_FIELD_START:
state = S.HEADER_FIELD
mark('headerField')
index = 0
case S.HEADER_FIELD:
if (c == CR) {
clear('headerField')
state = S.HEADERS_ALMOST_DONE
break
}
index++
if (c == HYPHEN) {
break
}
if (c == COLON) {
if (index == 1) {
// empty header field
return i
}
dataCallback('headerField', true)
state = S.HEADER_VALUE_START
break
}
cl = lower(c)
if (cl < A || cl > Z) {
return i
}
break
case S.HEADER_VALUE_START:
if (c == SPACE) {
break
}
mark('headerValue')
state = S.HEADER_VALUE
case S.HEADER_VALUE:
if (c == CR) {
dataCallback('headerValue', true)
callback('headerEnd')
state = S.HEADER_VALUE_ALMOST_DONE
}
break
case S.HEADER_VALUE_ALMOST_DONE:
if (c != LF) {
return i
}
state = S.HEADER_FIELD_START
break
case S.HEADERS_ALMOST_DONE:
if (c != LF) {
return i
}
callback('headersEnd')
state = S.PART_DATA_START
break
case S.PART_DATA_START:
state = S.PART_DATA
mark('partData')
case S.PART_DATA:
prevIndex = index
if (index === 0) {
// boyer-moore derrived algorithm to safely skip non-boundary data
i += boundaryEnd
while (i < bufferLength && !(buffer[i] in boundaryChars)) {
i += boundaryLength
}
i -= boundaryEnd
c = buffer[i]
}
if (index < boundary.length) {
if (boundary[index] == c) {
if (index === 0) {
dataCallback('partData', true)
}
index++
} else {
index = 0
}
} else if (index == boundary.length) {
index++
if (c == CR) {
// CR = part boundary
flags |= F.PART_BOUNDARY
} else if (c == HYPHEN) {
// HYPHEN = end boundary
flags |= F.LAST_BOUNDARY
} else {
index = 0
}
} else if (index - 1 == boundary.length) {
if (flags & F.PART_BOUNDARY) {
index = 0
if (c == LF) {
// unset the PART_BOUNDARY flag
flags &= ~F.PART_BOUNDARY
callback('partEnd')
callback('partBegin')
state = S.HEADER_FIELD_START
break
}
} else if (flags & F.LAST_BOUNDARY) {
if (c == HYPHEN) {
callback('partEnd')
callback('end')
state = S.END
flags = 0
} else {
index = 0
}
} else {
index = 0
}
}
if (index > 0) {
// when matching a possible boundary, keep a lookbehind reference
// in case it turns out to be a false lead
lookbehind[index - 1] = c
} else if (prevIndex > 0) {
// if our boundary turned out to be rubbish, the captured lookbehind
// belongs to partData
callback('partData', lookbehind, 0, prevIndex)
prevIndex = 0
mark('partData')
// reconsider the current character even so it interrupted the sequence
// it could be the beginning of a new sequence
i--
}
break
case S.END:
break
default:
return i
}
}
dataCallback('headerField')
dataCallback('headerValue')
dataCallback('partData')
this.index = index
this.state = state
this.flags = flags
return len
}
MultipartParser.prototype.end = function() {
var callback = function(self, name) {
var callbackSymbol = 'on' + name.substr(0, 1).toUpperCase() + name.substr(1)
if (callbackSymbol in self) {
self[callbackSymbol]()
}
}
if (
(this.state == S.HEADER_FIELD_START && this.index === 0) ||
(this.state == S.PART_DATA && this.index == this.boundary.length)
) {
callback(this, 'partEnd')
callback(this, 'end')
} else if (this.state != S.END) {
return new Error(
'MultipartParser.end(): stream ended unexpectedly: ' + this.explain()
)
}
}
MultipartParser.prototype.explain = function() {
return 'state = ' + MultipartParser.stateToString(this.state)
}

17
lib/octet_parser.js Normal file
View File

@ -0,0 +1,17 @@
import { EventEmitter } from 'events'
import util from 'util'
export function OctetParser() {
EventEmitter.call(this)
}
util.inherits(OctetParser, EventEmitter)
OctetParser.prototype.write = function(buffer) {
this.emit('data', buffer)
return buffer.length
}
OctetParser.prototype.end = function() {
this.emit('end')
}

27
lib/querystring_parser.js Normal file
View File

@ -0,0 +1,27 @@
// This is a buffering parser, not quite as nice as the multipart one.
// If I find time I'll rewrite this to be fully streaming as well
import querystring from 'querystring'
export class QuerystringParser {
constructor(maxKeys) {
this.maxKeys = maxKeys
this.buffer = ''
}
write(buffer) {
this.buffer += buffer.toString('ascii')
return buffer.length
}
end() {
var fields = querystring.parse(this.buffer, '&', '=', {
maxKeys: this.maxKeys
})
for (var field in fields) {
this.onField(field, fields[field])
}
this.buffer = ''
this.onEnd()
}
}

15
package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "@gm5/request",
"version": "1.0.0",
"description": "对Http的request进一步封装, 提供常用的API",
"main": "index.js",
"author": "yutent",
"type": "module",
"keywords": ["five", "node-five", "five.js", "fivejs", "request", "http"],
"dependencies": {
"@bytedo/es.shim": "^1.0.0",
"iofs": "^1.1.0"
},
"repository": "https://github.com/bytedo/gmf.request.git",
"license": "MIT"
}