You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					465 lines
				
				22 KiB
			
		
		
			
		
	
	
					465 lines
				
				22 KiB
			| 
											3 years ago
										 | # urllib@2
 | ||
|  | 
 | ||
|  | [![NPM version][npm-image]][npm-url] | ||
|  | [](https://github.com/node-modules/urllib/actions/workflows/nodejs.yml) | ||
|  | [![Test coverage][codecov-image]][codecov-url] | ||
|  | [![Known Vulnerabilities][snyk-image]][snyk-url] | ||
|  | [![npm download][download-image]][download-url] | ||
|  | 
 | ||
|  | [npm-image]: https://img.shields.io/npm/v/urllib.svg?style=flat-square | ||
|  | [npm-url]: https://npmjs.org/package/urllib | ||
|  | [codecov-image]: https://codecov.io/gh/node-modules/urllib/branch/master/graph/badge.svg | ||
|  | [codecov-url]: https://codecov.io/gh/node-modules/urllib | ||
|  | [snyk-image]: https://snyk.io/test/npm/urllib/badge.svg?style=flat-square | ||
|  | [snyk-url]: https://snyk.io/test/npm/urllib | ||
|  | [download-image]: https://img.shields.io/npm/dm/urllib.svg?style=flat-square | ||
|  | [download-url]: https://npmjs.org/package/urllib | ||
|  | 
 | ||
|  | Request HTTP URLs in a complex world — basic | ||
|  | and digest authentication, redirections, cookies, timeout and more. | ||
|  | 
 | ||
|  | ## Install
 | ||
|  | 
 | ||
|  | ```bash | ||
|  | $ npm install urllib@2 --save | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Usage
 | ||
|  | 
 | ||
|  | ### callback
 | ||
|  | 
 | ||
|  | ```js | ||
|  | var urllib = require('urllib'); | ||
|  | 
 | ||
|  | urllib.request('http://cnodejs.org/', function (err, data, res) { | ||
|  |   if (err) { | ||
|  |     throw err; // you need to handle error | ||
|  |   } | ||
|  |   console.log(res.statusCode); | ||
|  |   console.log(res.headers); | ||
|  |   // data is Buffer instance | ||
|  |   console.log(data.toString()); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Promise
 | ||
|  | 
 | ||
|  | If you've installed [bluebird][bluebird], | ||
|  | [bluebird][bluebird] will be used. | ||
|  | `urllib` does not install [bluebird][bluebird] for you. | ||
|  | 
 | ||
|  | Otherwise, if you're using a node that has native v8 Promises (v0.11.13+), | ||
|  | then that will be used. | ||
|  | 
 | ||
|  | Otherwise, this library will crash the process and exit, | ||
|  | so you might as well install [bluebird][bluebird] as a dependency! | ||
|  | 
 | ||
|  | ```js | ||
|  | var urllib = require('urllib'); | ||
|  | 
 | ||
|  | urllib.request('http://nodejs.org').then(function (result) { | ||
|  |   // result: {data: buffer, res: response object} | ||
|  |   console.log('status: %s, body size: %d, headers: %j', result.res.statusCode, result.data.length, result.res.headers); | ||
|  | }).catch(function (err) { | ||
|  |   console.error(err); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | ### co & generator
 | ||
|  | 
 | ||
|  | If you are using [co](https://github.com/visionmedia/co) or [koa](https://github.com/koajs/koa): | ||
|  | 
 | ||
|  | ```js | ||
|  | var co = require('co'); | ||
|  | var urllib = require('urllib'); | ||
|  | 
 | ||
|  | co(function* () { | ||
|  |   var result = yield urllib.requestThunk('http://nodejs.org'); | ||
|  |   console.log('status: %s, body size: %d, headers: %j', | ||
|  |     result.status, result.data.length, result.headers); | ||
|  | })(); | ||
|  | ``` | ||
|  | 
 | ||
|  | ## Global `response` event
 | ||
|  | 
 | ||
|  | You should create a urllib instance first. | ||
|  | 
 | ||
|  | ```js | ||
|  | var httpclient = require('urllib').create(); | ||
|  | 
 | ||
|  | httpclient.on('response', function (info) { | ||
|  |   error: err, | ||
|  |   ctx: args.ctx, | ||
|  |   req: { | ||
|  |     url: url, | ||
|  |     options: options, | ||
|  |     size: requestSize, | ||
|  |   }, | ||
|  |   res: res | ||
|  | }); | ||
|  | 
 | ||
|  | httpclient.request('http://nodejs.org', function (err, body) { | ||
|  |   console.log('body size: %d', body.length); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | ## API Doc
 | ||
|  | 
 | ||
|  | ### Method: `http.request(url[, options][, callback])`
 | ||
|  | 
 | ||
|  | #### Arguments
 | ||
|  | 
 | ||
|  | - **url** String | Object - The URL to request, either a String or a Object that return by [url.parse](http://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost). | ||
|  | - ***options*** Object - Optional | ||
|  |     - ***method*** String - Request method, defaults to `GET`. Could be `GET`, `POST`, `DELETE` or `PUT`. Alias 'type'. | ||
|  |     - ***data*** Object - Data to be sent. Will be stringify automatically. | ||
|  |     - ***dataAsQueryString*** Boolean - Force convert `data` to query string. | ||
|  |     - ***content*** String | [Buffer](http://nodejs.org/api/buffer.html) - Manually set the content of payload. If set, `data` will be ignored. | ||
|  |     - ***stream*** [stream.Readable](http://nodejs.org/api/stream.html#stream_class_stream_readable) - Stream to be pipe to the remote. If set, `data` and `content` will be ignored. | ||
|  |     - ***writeStream*** [stream.Writable](http://nodejs.org/api/stream.html#stream_class_stream_writable) - A writable stream to be piped by the response stream. Responding data will be write to this stream and `callback` will be called with `data` set `null` after finished writing. | ||
|  |     - ***files*** {Array<ReadStream|Buffer|String> | Object | ReadStream | Buffer | String - The files will send with `multipart/form-data` format, base on `formstream`. If `method` not set, will use `POST` method by default. | ||
|  |     - ***consumeWriteStream*** [true] - consume the writeStream, invoke the callback after writeStream close. | ||
|  |     - ***contentType*** String - Type of request data. Could be `json` (**Notes**: not use `application/json` here). If it's `json`, will auto set `Content-Type: application/json` header. | ||
|  |     - ***nestedQuerystring*** Boolean - urllib default use querystring to stringify form data which don't support nested object, will use [qs](https://github.com/ljharb/qs) instead of querystring to support nested object by set this option to true. | ||
|  |     - ***dataType*** String - Type of response data. Could be `text` or `json`. If it's `text`, the `callback`ed `data` would be a String. If it's `json`, the `data` of callback would be a parsed JSON Object and will auto set `Accept: application/json` header. Default `callback`ed `data` would be a `Buffer`. | ||
|  |     - **fixJSONCtlChars** Boolean - Fix the control characters (U+0000 through U+001F) before JSON parse response. Default is `false`. | ||
|  |     - ***headers*** Object - Request headers. | ||
|  |     - ***keepHeaderCase*** Boolean - by default will convert header keys to lowercase | ||
|  |     - ***timeout*** Number | Array - Request timeout in milliseconds for connecting phase and response receiving phase. Defaults to `exports.TIMEOUT`, both are 5s. You can use `timeout: 5000` to tell urllib use same timeout on two phase or set them seperately such as `timeout: [3000, 5000]`, which will set connecting timeout to 3s and response 5s. | ||
|  |     - ***auth*** String - `username:password` used in HTTP Basic Authorization. | ||
|  |     - ***digestAuth*** String - `username:password` used in HTTP [Digest Authorization](http://en.wikipedia.org/wiki/Digest_access_authentication). | ||
|  |     - ***agent*** [http.Agent](http://nodejs.org/api/http.html#http_class_http_agent) - HTTP Agent object. | ||
|  |       Set `false` if you does not use agent. | ||
|  |     - ***httpsAgent*** [https.Agent](http://nodejs.org/api/https.html#https_class_https_agent) - HTTPS Agent object. | ||
|  |       Set `false` if you does not use agent. | ||
|  |     - ***ca*** String | Buffer | Array - An array of strings or Buffers of trusted certificates. | ||
|  |       If this is omitted several well known "root" CAs will be used, like VeriSign. | ||
|  |       These are used to authorize connections. | ||
|  |       **Notes**: This is necessary only if the server uses the self-signed certificate | ||
|  |     - ***rejectUnauthorized*** Boolean - If true, the server certificate is verified against the list of supplied CAs. | ||
|  |       An 'error' event is emitted if verification fails. Default: true. | ||
|  |     - ***pfx*** String | Buffer - A string or Buffer containing the private key, | ||
|  |       certificate and CA certs of the server in PFX or PKCS12 format. | ||
|  |     - ***key*** String | Buffer - A string or Buffer containing the private key of the client in PEM format. | ||
|  |       **Notes**: This is necessary only if using the client certificate authentication | ||
|  |     - ***cert*** String | Buffer - A string or Buffer containing the certificate key of the client in PEM format. | ||
|  |       **Notes**: This is necessary only if using the client certificate authentication | ||
|  |     - ***passphrase*** String - A string of passphrase for the private key or pfx. | ||
|  |     - ***ciphers*** String - A string describing the ciphers to use or exclude. | ||
|  |     - ***secureProtocol*** String - The SSL method to use, e.g. SSLv3_method to force SSL version 3. | ||
|  |     - ***followRedirect*** Boolean - follow HTTP 3xx responses as redirects. defaults to false. | ||
|  |     - ***maxRedirects*** Number - The maximum number of redirects to follow, defaults to 10. | ||
|  |     - ***formatRedirectUrl*** Function - Format the redirect url by your self. Default is `url.resolve(from, to)`. | ||
|  |     - ***beforeRequest*** Function - Before request hook, you can change every thing here. | ||
|  |     - ***streaming*** Boolean - let you get the `res` object when request  connected, default `false`. alias `customResponse` | ||
|  |     - ***gzip*** Boolean - Accept gzip response content and auto decode it, default is `false`. | ||
|  |     - ***timing*** Boolean - Enable timing or not, default is `false`. | ||
|  |     - ***enableProxy*** Boolean - Enable proxy request, default is `false`. | ||
|  |     - ***proxy*** String | Object - proxy agent uri or options, default is `null`. | ||
|  |     - ***lookup*** Function - Custom DNS lookup function, default is `dns.lookup`. Require node >= 4.0.0(for http protocol) and node >=8(for https protocol) | ||
|  |     - ***checkAddress*** Function: optional, check request address to protect from SSRF and similar attacks. It receive tow arguments(`ip` and `family`) and should return true or false to identified the address is legal or not. It rely on `lookup` and have the same version requirement. | ||
|  |     - ***trace*** Boolean - Enable capture stack include call site of library entrance, default is `false`. | ||
|  |     - ***socketPath*** String - optional Unix Domain Socket. (Refer to [Node.js Document](https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_http_request_options_callback)) | ||
|  | - ***callback(err, data, res)*** Function - Optional callback. | ||
|  |     - **err** Error - Would be `null` if no error accured. | ||
|  |     - **data** Buffer | Object - The data responsed. Would be a Buffer if `dataType` is set to `text` or an JSON parsed into Object if it's set to `json`. | ||
|  |     - **res** [http.IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) - The response. | ||
|  | 
 | ||
|  | #### Returns
 | ||
|  | 
 | ||
|  | [http.ClientRequest](http://nodejs.org/api/http.html#http_class_http_clientrequest) - The request. | ||
|  | 
 | ||
|  | Calling `.abort()` method of the request stream can cancel the request. | ||
|  | 
 | ||
|  | #### Options: `options.data`
 | ||
|  | 
 | ||
|  | When making a request: | ||
|  | 
 | ||
|  | ```js | ||
|  | urllib.request('http://example.com', { | ||
|  |   method: 'GET', | ||
|  |   data: { | ||
|  |     'a': 'hello', | ||
|  |     'b': 'world' | ||
|  |   } | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | For `GET` request, `data` will be stringify to query string, e.g. `http://example.com/?a=hello&b=world`. | ||
|  | 
 | ||
|  | For others like `POST`, `PATCH` or `PUT` request, | ||
|  | in defaults, the `data` will be stringify into `application/x-www-form-urlencoded` format | ||
|  | if `Content-Type` header is not set. | ||
|  | 
 | ||
|  | If `Content-type` is `application/json`, the `data` will be `JSON.stringify` to JSON data format. | ||
|  | 
 | ||
|  | #### Options: `options.content`
 | ||
|  | 
 | ||
|  | `options.content` is useful when you wish to construct the request body by yourself, | ||
|  | for example making a `Content-Type: application/json` request. | ||
|  | 
 | ||
|  | Notes that if you want to send a JSON body, you should stringify it yourself: | ||
|  | 
 | ||
|  | ```js | ||
|  | urllib.request('http://example.com', { | ||
|  |   method: 'POST', | ||
|  |   headers: { | ||
|  |     'Content-Type': 'application/json' | ||
|  |   }, | ||
|  |   content: JSON.stringify({ | ||
|  |     a: 'hello', | ||
|  |     b: 'world' | ||
|  |   }) | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | It would make a HTTP request like: | ||
|  | 
 | ||
|  | ```http | ||
|  | POST / HTTP/1.1 | ||
|  | Host: example.com | ||
|  | Content-Type: application/json | ||
|  | 
 | ||
|  | { | ||
|  |   "a": "hello", | ||
|  |   "b": "world" | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | This exmaple can use `options.data` with `application/json` content type: | ||
|  | 
 | ||
|  | ```js | ||
|  | urllib.request('http://example.com', { | ||
|  |   method: 'POST', | ||
|  |   headers: { | ||
|  |     'Content-Type': 'application/json' | ||
|  |   }, | ||
|  |   data: { | ||
|  |     a: 'hello', | ||
|  |     b: 'world' | ||
|  |   } | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | #### Options: `options.files`
 | ||
|  | 
 | ||
|  | Upload a file with a `hello` field. | ||
|  | 
 | ||
|  | ```js | ||
|  | var urllib = request('urllib'); | ||
|  | 
 | ||
|  | var req = urllib.request('http://my.server.com/upload', { | ||
|  |   files: __filename, | ||
|  |   data: { | ||
|  |     hello: 'hello urllib', | ||
|  |   }, | ||
|  | }, function (err, data, res) { | ||
|  |   // upload finished | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | Upload multi files with a `hello` field. | ||
|  | 
 | ||
|  | ```js | ||
|  | var urllib = request('urllib'); | ||
|  | 
 | ||
|  | var req = urllib.request('http://my.server.com/upload', { | ||
|  |   files: [ | ||
|  |     __filename, | ||
|  |     fs.createReadStream(__filename), | ||
|  |     Buffer.from('mock file content'), | ||
|  |   ], | ||
|  |   data: { | ||
|  |     hello: 'hello urllib with multi files', | ||
|  |   }, | ||
|  | }, function (err, data, res) { | ||
|  |   // upload finished | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | Custom file field name with `uploadfile`. | ||
|  | 
 | ||
|  | ```js | ||
|  | var urllib = request('urllib'); | ||
|  | 
 | ||
|  | var req = urllib.request('http://my.server.com/upload', { | ||
|  |   files: { | ||
|  |     uploadfile: __filename, | ||
|  |   }, | ||
|  | }, function (err, data, res) { | ||
|  |   // upload finished | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | #### Options: `options.stream`
 | ||
|  | 
 | ||
|  | Uploads a file with [formstream](https://github.com/node-modules/formstream): | ||
|  | 
 | ||
|  | ```js | ||
|  | var urllib = require('urllib'); | ||
|  | var formstream = require('formstream'); | ||
|  | 
 | ||
|  | var form = formstream(); | ||
|  | form.file('file', __filename); | ||
|  | form.field('hello', '你好urllib'); | ||
|  | 
 | ||
|  | var req = urllib.request('http://my.server.com/upload', { | ||
|  |   method: 'POST', | ||
|  |   headers: form.headers(), | ||
|  |   stream: form | ||
|  | }, function (err, data, res) { | ||
|  |   // upload finished | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Response Object
 | ||
|  | 
 | ||
|  | Response is normal object, it contains: | ||
|  | 
 | ||
|  | * `status` or `statusCode`: response status code. | ||
|  |   * `-1` meaning some network error like `ENOTFOUND` | ||
|  |   * `-2` meaning ConnectionTimeoutError | ||
|  | * `statusMessage`: response status message. | ||
|  | * `headers`: response http headers, default is `{}` | ||
|  | * `size`: response size | ||
|  | * `aborted`: response was aborted or not | ||
|  | * `rt`: total request and response time in ms. | ||
|  | * `timing`: timing object if timing enable. | ||
|  | * `remoteAddress`: http server ip address | ||
|  | * `remotePort`: http server ip port | ||
|  | * `socketHandledRequests`: socket already handled request count | ||
|  | * `socketHandledResponses`: socket already handled response count | ||
|  | 
 | ||
|  | #### Response: `res.aborted`
 | ||
|  | 
 | ||
|  | If the underlaying connection was terminated before `response.end()` was called, | ||
|  | `res.aborted` should be `true`. | ||
|  | 
 | ||
|  | ```js | ||
|  | require('http').createServer(function (req, res) { | ||
|  |   req.resume(); | ||
|  |   req.on('end', function () { | ||
|  |     res.write('foo haha\n'); | ||
|  |     setTimeout(function () { | ||
|  |       res.write('foo haha 2'); | ||
|  |       setTimeout(function () { | ||
|  |         res.socket.end(); | ||
|  |       }, 300); | ||
|  |     }, 200); | ||
|  |     return; | ||
|  |   }); | ||
|  | }).listen(1984); | ||
|  | 
 | ||
|  | urllib.request('http://127.0.0.1:1984/socket.end', function (err, data, res) { | ||
|  |   data.toString().should.equal('foo haha\nfoo haha 2'); | ||
|  |   should.ok(res.aborted); | ||
|  |   done(); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | ### HttpClient2
 | ||
|  | 
 | ||
|  | HttpClient2 is a new instance for future. request method only return a promise, compatible with `async/await` and generator in co. | ||
|  | 
 | ||
|  | #### Options
 | ||
|  | 
 | ||
|  | options extends from urllib, besides below | ||
|  | 
 | ||
|  | - ***retry*** Number - a retry count, when get an error, it will request again until reach the retry count. | ||
|  | - ***retryDelay*** Number - wait a delay(ms) between retries. | ||
|  | - ***isRetry*** Function - determine whether retry, a response object as the first argument. it will retry when status >= 500 by default. Request error is not included. | ||
|  | 
 | ||
|  | #### Warning
 | ||
|  | 
 | ||
|  | It's not supported by using retry and writeStream, because the retry request can't stop the stream which is consuming. | ||
|  | 
 | ||
|  | ## Proxy
 | ||
|  | 
 | ||
|  | Support both `http` and `https` protocol. | ||
|  | 
 | ||
|  | **Notice: Only support on Node.js >= 4.0.0** | ||
|  | 
 | ||
|  | ### Programming
 | ||
|  | 
 | ||
|  | ```js | ||
|  | urllib.request('https://twitter.com/', { | ||
|  |   enableProxy: true, | ||
|  |   proxy: 'http://localhost:8008', | ||
|  | }, (err, data, res) => { | ||
|  |   console.log(res.status, res.headers); | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | ### System environment variable
 | ||
|  | 
 | ||
|  | - http | ||
|  | 
 | ||
|  | ```bash | ||
|  | HTTP_PROXY=http://localhost:8008 | ||
|  | http_proxy=http://localhost:8008 | ||
|  | ``` | ||
|  | 
 | ||
|  | - https | ||
|  | 
 | ||
|  | ```bash | ||
|  | HTTP_PROXY=http://localhost:8008 | ||
|  | http_proxy=http://localhost:8008 | ||
|  | HTTPS_PROXY=https://localhost:8008 | ||
|  | https_proxy=https://localhost:8008 | ||
|  | ``` | ||
|  | 
 | ||
|  | ```bash | ||
|  | $ http_proxy=http://localhost:8008 node index.js | ||
|  | ``` | ||
|  | 
 | ||
|  | ### Trace
 | ||
|  | If set trace true, error stack will contains full call stack, like | ||
|  | ``` | ||
|  | Error: connect ECONNREFUSED 127.0.0.1:11 | ||
|  |     at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1113:14) | ||
|  |     -------------------- | ||
|  |     at ~/workspace/urllib/lib/urllib.js:150:13 | ||
|  |     at new Promise (<anonymous>) | ||
|  |     at Object.request (~/workspace/urllib/lib/urllib.js:149:10) | ||
|  |     at Context.<anonymous> (~/workspace/urllib/test/urllib_promise.test.js:49:19) | ||
|  |     .... | ||
|  | ``` | ||
|  | 
 | ||
|  | When open the trace, urllib may have poor perfomance, please consider carefully. | ||
|  | 
 | ||
|  | ## TODO
 | ||
|  | 
 | ||
|  | * [ ] Support component | ||
|  | * [ ] Browser env use Ajax | ||
|  | * [√] Support Proxy | ||
|  | * [√] Upload file like form upload | ||
|  | * [√] Auto redirect handle | ||
|  | * [√] https & self-signed certificate | ||
|  | * [√] Connection timeout & Response timeout | ||
|  | * [√] Support `Accept-Encoding=gzip` by `options.gzip = true` | ||
|  | * [√] Support [Digest access authentication](http://en.wikipedia.org/wiki/Digest_access_authentication) | ||
|  | 
 | ||
|  | <!-- GITCONTRIBUTOR_START --> | ||
|  | 
 | ||
|  | ## Contributors
 | ||
|  | 
 | ||
|  | |[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/985607?v=4" width="100px;"/><br/><sub><b>dead-horse</b></sub>](https://github.com/dead-horse)<br/>|[<img src="https://avatars.githubusercontent.com/u/288288?v=4" width="100px;"/><br/><sub><b>xingrz</b></sub>](https://github.com/xingrz)<br/>|[<img src="https://avatars.githubusercontent.com/u/360661?v=4" width="100px;"/><br/><sub><b>popomore</b></sub>](https://github.com/popomore)<br/>|[<img src="https://avatars.githubusercontent.com/u/327019?v=4" width="100px;"/><br/><sub><b>JacksonTian</b></sub>](https://github.com/JacksonTian)<br/>|[<img src="https://avatars.githubusercontent.com/u/543405?v=4" width="100px;"/><br/><sub><b>ibigbug</b></sub>](https://github.com/ibigbug)<br/>| | ||
|  | | :---: | :---: | :---: | :---: | :---: | :---: | | ||
|  | |[<img src="https://avatars.githubusercontent.com/u/14790466?v=4" width="100px;"/><br/><sub><b>greenkeeperio-bot</b></sub>](https://github.com/greenkeeperio-bot)<br/>|[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|[<img src="https://avatars.githubusercontent.com/u/5381764?v=4" width="100px;"/><br/><sub><b>paambaati</b></sub>](https://github.com/paambaati)<br/>|[<img src="https://avatars.githubusercontent.com/u/1433247?v=4" width="100px;"/><br/><sub><b>denghongcai</b></sub>](https://github.com/denghongcai)<br/>|[<img src="https://avatars.githubusercontent.com/u/2842176?v=4" width="100px;"/><br/><sub><b>XadillaX</b></sub>](https://github.com/XadillaX)<br/>|[<img src="https://avatars.githubusercontent.com/u/1147375?v=4" width="100px;"/><br/><sub><b>alsotang</b></sub>](https://github.com/alsotang)<br/>| | ||
|  | |[<img src="https://avatars.githubusercontent.com/u/546535?v=4" width="100px;"/><br/><sub><b>leoner</b></sub>](https://github.com/leoner)<br/>|[<img src="https://avatars.githubusercontent.com/u/19908330?v=4" width="100px;"/><br/><sub><b>hyj1991</b></sub>](https://github.com/hyj1991)<br/>|[<img src="https://avatars.githubusercontent.com/u/1747852?v=4" width="100px;"/><br/><sub><b>isayme</b></sub>](https://github.com/isayme)<br/>|[<img src="https://avatars.githubusercontent.com/u/6897780?v=4" width="100px;"/><br/><sub><b>killagu</b></sub>](https://github.com/killagu)<br/>|[<img src="https://avatars.githubusercontent.com/u/252317?v=4" width="100px;"/><br/><sub><b>cyjake</b></sub>](https://github.com/cyjake)<br/>|[<img src="https://avatars.githubusercontent.com/u/5856440?v=4" width="100px;"/><br/><sub><b>whxaxes</b></sub>](https://github.com/whxaxes)<br/>| | ||
|  | |[<img src="https://avatars.githubusercontent.com/u/309219?v=4" width="100px;"/><br/><sub><b>chadxz</b></sub>](https://github.com/chadxz)<br/>|[<img src="https://avatars.githubusercontent.com/u/5139554?v=4" width="100px;"/><br/><sub><b>danielwpz</b></sub>](https://github.com/danielwpz)<br/>|[<img src="https://avatars.githubusercontent.com/u/5127897?v=4" width="100px;"/><br/><sub><b>danielsss</b></sub>](https://github.com/danielsss)<br/>|[<img src="https://avatars.githubusercontent.com/u/3367820?v=4" width="100px;"/><br/><sub><b>Jeff-Tian</b></sub>](https://github.com/Jeff-Tian)<br/>|[<img src="https://avatars.githubusercontent.com/u/32407?v=4" width="100px;"/><br/><sub><b>jedahan</b></sub>](https://github.com/jedahan)<br/>|[<img src="https://avatars.githubusercontent.com/u/17075261?v=4" width="100px;"/><br/><sub><b>nick-ng</b></sub>](https://github.com/nick-ng)<br/>| | ||
|  | |[<img src="https://avatars.githubusercontent.com/u/1706595?v=4" width="100px;"/><br/><sub><b>rishavsharan</b></sub>](https://github.com/rishavsharan)<br/>|[<img src="https://avatars.githubusercontent.com/u/1886161?v=4" width="100px;"/><br/><sub><b>willizm</b></sub>](https://github.com/willizm)<br/>|[<img src="https://avatars.githubusercontent.com/u/7227589?v=4" width="100px;"/><br/><sub><b>davidkhala</b></sub>](https://github.com/davidkhala)<br/>|[<img src="https://avatars.githubusercontent.com/u/535479?v=4" width="100px;"/><br/><sub><b>aleafs</b></sub>](https://github.com/aleafs)<br/>|[<img src="https://avatars.githubusercontent.com/u/3689968?v=4" width="100px;"/><br/><sub><b>Amunu</b></sub>](https://github.com/Amunu)<br/>|[<img src="https://avatars.githubusercontent.com/in/9426?v=4" width="100px;"/><br/><sub><b>azure-pipelines[bot]</b></sub>](https://github.com/apps/azure-pipelines)<br/>| | ||
|  | |[<img src="https://avatars.githubusercontent.com/u/1281323?v=4" width="100px;"/><br/><sub><b>changzhiwin</b></sub>](https://github.com/changzhiwin)<br/>|[<img src="https://avatars.githubusercontent.com/u/929503?v=4" width="100px;"/><br/><sub><b>yuzhigang33</b></sub>](https://github.com/yuzhigang33)<br/>|[<img src="https://avatars.githubusercontent.com/u/981128?v=4" width="100px;"/><br/><sub><b>fishbar</b></sub>](https://github.com/fishbar)<br/>|[<img src="https://avatars.githubusercontent.com/u/1207064?v=4" width="100px;"/><br/><sub><b>gxcsoccer</b></sub>](https://github.com/gxcsoccer)<br/>|[<img src="https://avatars.githubusercontent.com/u/17476119?v=4" width="100px;"/><br/><sub><b>mars-coder</b></sub>](https://github.com/mars-coder)<br/>|[<img src="https://avatars.githubusercontent.com/u/929179?v=4" width="100px;"/><br/><sub><b>rockdai</b></sub>](https://github.com/rockdai)<br/>| | ||
|  | [<img src="https://avatars.githubusercontent.com/u/2196373?v=4" width="100px;"/><br/><sub><b>dickeylth</b></sub>](https://github.com/dickeylth)<br/>|[<img src="https://avatars.githubusercontent.com/u/13050025?v=4" width="100px;"/><br/><sub><b>aladdin-add</b></sub>](https://github.com/aladdin-add)<br/> | ||
|  | 
 | ||
|  | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Tue Jul 05 2022 16:17:31 GMT+0800`. | ||
|  | 
 | ||
|  | <!-- GITCONTRIBUTOR_END --> | ||
|  | 
 | ||
|  | ## License
 | ||
|  | 
 | ||
|  | [MIT](LICENSE) | ||
|  | 
 | ||
|  | 
 | ||
|  | [bluebird]: https://github.com/petkaantonov/bluebird |