mirror of https://github.com/jkjoy/sunpeiwen.git
257 lines
7.0 KiB
JavaScript
257 lines
7.0 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const zlib = require('zlib');
|
||
|
const http = require('http');
|
||
|
const https = require('https');
|
||
|
const parse = require('url').parse;
|
||
|
const format = require('url').format;
|
||
|
|
||
|
const debugBody = require('debug')('httpx:body');
|
||
|
const debugHeader = require('debug')('httpx:header');
|
||
|
|
||
|
const httpAgent = new http.Agent({ keepAlive: true });
|
||
|
const httpsAgent = new https.Agent({ keepAlive: true });
|
||
|
|
||
|
const TIMEOUT = 3000; // 3s
|
||
|
|
||
|
const READ_TIMER = Symbol('TIMER::READ_TIMER');
|
||
|
const READ_TIME_OUT = Symbol('TIMER::READ_TIME_OUT');
|
||
|
const READ_TIMER_START_AT = Symbol('TIMER::READ_TIMER_START_AT');
|
||
|
|
||
|
var append = function (err, name, message) {
|
||
|
err.name = name + err.name;
|
||
|
err.message = `${message}. ${err.message}`;
|
||
|
return err;
|
||
|
};
|
||
|
|
||
|
const isNumber = function (num) {
|
||
|
return num !== null && !isNaN(num);
|
||
|
};
|
||
|
|
||
|
exports.request = function (url, opts) {
|
||
|
// request(url)
|
||
|
opts || (opts = {});
|
||
|
|
||
|
const parsed = typeof url === 'string' ? parse(url) : url;
|
||
|
|
||
|
let readTimeout, connectTimeout;
|
||
|
if (isNumber(opts.readTimeout) || isNumber(opts.connectTimeout)) {
|
||
|
readTimeout = isNumber(opts.readTimeout) ? Number(opts.readTimeout) : TIMEOUT;
|
||
|
connectTimeout = isNumber(opts.connectTimeout) ? Number(opts.connectTimeout) : TIMEOUT;
|
||
|
} else if (isNumber(opts.timeout)) {
|
||
|
readTimeout = connectTimeout = Number(opts.timeout);
|
||
|
} else {
|
||
|
readTimeout = connectTimeout = TIMEOUT;
|
||
|
}
|
||
|
|
||
|
const isHttps = parsed.protocol === 'https:';
|
||
|
const method = (opts.method || 'GET').toUpperCase();
|
||
|
const defaultAgent = isHttps ? httpsAgent : httpAgent;
|
||
|
const agent = opts.agent || defaultAgent;
|
||
|
|
||
|
var options = {
|
||
|
host: parsed.hostname || 'localhost',
|
||
|
path: parsed.path || '/',
|
||
|
method: method,
|
||
|
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
||
|
agent: agent,
|
||
|
headers: opts.headers || {},
|
||
|
// ssl config
|
||
|
key: opts.key || '',
|
||
|
cert: opts.cert || '',
|
||
|
ca: opts.ca || '',
|
||
|
// connect timerout
|
||
|
timeout: connectTimeout
|
||
|
};
|
||
|
|
||
|
if (isHttps && typeof opts.rejectUnauthorized !== 'undefined') {
|
||
|
options.rejectUnauthorized = opts.rejectUnauthorized;
|
||
|
}
|
||
|
|
||
|
if (opts.compression) {
|
||
|
options.headers['accept-encoding'] = 'gzip,deflate';
|
||
|
}
|
||
|
|
||
|
const httplib = isHttps ? https : http;
|
||
|
|
||
|
if (typeof opts.beforeRequest === 'function') {
|
||
|
options = opts.beforeRequest(options);
|
||
|
}
|
||
|
|
||
|
return new Promise((resolve, reject) => {
|
||
|
const request = httplib.request(options);
|
||
|
const body = opts.data;
|
||
|
|
||
|
var fulfilled = (response) => {
|
||
|
if (debugHeader.enabled) {
|
||
|
const requestHeaders = response.req._header;
|
||
|
requestHeaders.split('\r\n').forEach((line) => {
|
||
|
debugHeader('> %s', line);
|
||
|
});
|
||
|
|
||
|
debugHeader('< HTTP/%s %s %s', response.httpVersion, response.statusCode, response.statusMessage);
|
||
|
Object.keys(response.headers).forEach((key) => {
|
||
|
debugHeader('< %s: %s', key, response.headers[key]);
|
||
|
});
|
||
|
}
|
||
|
resolve(response);
|
||
|
};
|
||
|
|
||
|
var rejected = (err) => {
|
||
|
err.message += `${method} ${format(parsed)} failed.`;
|
||
|
// clear response timer when error
|
||
|
if (request.socket && request.socket[READ_TIMER]) {
|
||
|
clearTimeout(request.socket[READ_TIMER]);
|
||
|
}
|
||
|
reject(err);
|
||
|
};
|
||
|
|
||
|
var abort = (err) => {
|
||
|
request.abort();
|
||
|
rejected(err);
|
||
|
};
|
||
|
|
||
|
const startResponseTimer = function (socket) {
|
||
|
const timer = setTimeout(() => {
|
||
|
if (socket[READ_TIMER]) {
|
||
|
clearTimeout(socket[READ_TIMER]);
|
||
|
socket[READ_TIMER] = null;
|
||
|
}
|
||
|
var err = new Error();
|
||
|
var message = `ReadTimeout(${readTimeout})`;
|
||
|
abort(append(err, 'RequestTimeout', message));
|
||
|
}, readTimeout);
|
||
|
// start read-timer
|
||
|
socket[READ_TIME_OUT] = readTimeout;
|
||
|
socket[READ_TIMER] = timer;
|
||
|
socket[READ_TIMER_START_AT] = Date.now();
|
||
|
};
|
||
|
|
||
|
// string
|
||
|
if (!body || 'string' === typeof body || body instanceof Buffer) {
|
||
|
if (debugBody.enabled) {
|
||
|
if (!body) {
|
||
|
debugBody('<no request body>');
|
||
|
} else if ('string' === typeof body) {
|
||
|
debugBody(body);
|
||
|
} else {
|
||
|
debugBody(`Buffer <ignored>, Buffer length: ${body.length}`);
|
||
|
}
|
||
|
}
|
||
|
request.end(body);
|
||
|
} else if ('function' === typeof body.pipe) { // stream
|
||
|
body.pipe(request);
|
||
|
if (debugBody.enabled) {
|
||
|
debugBody('<request body is a stream>');
|
||
|
}
|
||
|
body.once('error', (err) => {
|
||
|
abort(append(err, 'HttpX', 'Stream occor error'));
|
||
|
});
|
||
|
}
|
||
|
|
||
|
request.on('response', fulfilled);
|
||
|
request.on('error', rejected);
|
||
|
request.once('socket', function (socket) {
|
||
|
// reuse socket
|
||
|
if (socket.readyState === 'opening') {
|
||
|
socket.once('connect', function () {
|
||
|
startResponseTimer(socket);
|
||
|
});
|
||
|
} else {
|
||
|
startResponseTimer(socket);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
};
|
||
|
|
||
|
exports.read = function (response, encoding) {
|
||
|
var readable = response;
|
||
|
switch (response.headers['content-encoding']) {
|
||
|
// or, just use zlib.createUnzip() to handle both cases
|
||
|
case 'gzip':
|
||
|
readable = response.pipe(zlib.createGunzip());
|
||
|
break;
|
||
|
case 'deflate':
|
||
|
readable = response.pipe(zlib.createInflate());
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return new Promise((resolve, reject) => {
|
||
|
// node.js 14 use response.client
|
||
|
const socket = response.socket || response.client;
|
||
|
|
||
|
const makeReadTimeoutError = () => {
|
||
|
const req = response.req;
|
||
|
var err = new Error();
|
||
|
err.name = 'RequestTimeoutError';
|
||
|
err.message = `ReadTimeout: ${socket[READ_TIME_OUT]}. ${req.method} ${req.path} failed.`;
|
||
|
return err;
|
||
|
};
|
||
|
// check read-timer
|
||
|
let readTimer;
|
||
|
const oldReadTimer = socket[READ_TIMER];
|
||
|
if (!oldReadTimer) {
|
||
|
reject(makeReadTimeoutError());
|
||
|
return;
|
||
|
}
|
||
|
const remainTime = socket[READ_TIME_OUT] - (Date.now() - socket[READ_TIMER_START_AT]);
|
||
|
clearTimeout(oldReadTimer);
|
||
|
if (remainTime <= 0) {
|
||
|
reject(makeReadTimeoutError());
|
||
|
return;
|
||
|
}
|
||
|
readTimer = setTimeout(function () {
|
||
|
reject(makeReadTimeoutError());
|
||
|
}, remainTime);
|
||
|
|
||
|
// start reading data
|
||
|
var onError, onData, onEnd;
|
||
|
var cleanup = function () {
|
||
|
// cleanup
|
||
|
readable.removeListener('error', onError);
|
||
|
readable.removeListener('data', onData);
|
||
|
readable.removeListener('end', onEnd);
|
||
|
// clear read timer
|
||
|
if (readTimer) {
|
||
|
clearTimeout(readTimer);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const bufs = [];
|
||
|
var size = 0;
|
||
|
|
||
|
onData = function (buf) {
|
||
|
bufs.push(buf);
|
||
|
size += buf.length;
|
||
|
};
|
||
|
|
||
|
onError = function (err) {
|
||
|
cleanup();
|
||
|
reject(err);
|
||
|
};
|
||
|
|
||
|
onEnd = function () {
|
||
|
cleanup();
|
||
|
var buff = Buffer.concat(bufs, size);
|
||
|
|
||
|
debugBody('');
|
||
|
if (encoding) {
|
||
|
const result = buff.toString(encoding);
|
||
|
debugBody(result);
|
||
|
return resolve(result);
|
||
|
}
|
||
|
|
||
|
if (debugBody.enabled) {
|
||
|
debugBody(buff.toString());
|
||
|
}
|
||
|
resolve(buff);
|
||
|
};
|
||
|
|
||
|
readable.on('error', onError);
|
||
|
readable.on('data', onData);
|
||
|
readable.on('end', onEnd);
|
||
|
});
|
||
|
};
|