gohttp
Version:
http & https client for HTTP/1.1 and HTTP/2
743 lines (604 loc) • 18.5 kB
JavaScript
'use strict';
const process = require('node:process');
const http = require('node:http');
const https = require('node:https');
const crypto = require('node:crypto');
const fs = require('node:fs');
const urlparse = require('node:url');
const qs = require('./qs.js');
const bodymaker = require('./bodymaker.js');
const fmtpath = require('./fmtpath.js');
let gohttp = function (options = {}) {
if (! (this instanceof gohttp)) { return new gohttp(options); }
this.config = {
cert: '',
key: '',
ignoretls: true,
//不验证证书,针对HTTPS
//ignoreTLSAuth : true,
set ignoreTLSAuth (b) {
if (b) {
this.config.ignoretls = true;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
} else {
this.config.ignoretls = false;
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "1";
}
}
};
this.bodymaker = new bodymaker(options);
this.cert = '';
this.key = '';
if (this.ignoretls) {
this.cert = fs.readFileSync(this.config.cert);
this.key = fs.readFileSync(this.config.key);
}
};
gohttp.prototype.parseUrl = function (url) {
let u = new urlparse.URL(url);
let urlobj = {
hash : u.hash,
hostname : u.hostname,
protocol : u.protocol,
path : u.pathname,
pathname : u.pathname,
method : 'GET',
headers : {},
};
if (u.search.length > 0) {
urlobj.path += u.search;
}
if (u.protocol === 'unix:') {
urlobj.protocol = 'http:';
let sockarr = u.pathname.split('.sock');
urlobj.socketPath = `${sockarr[0]}.sock`;
urlobj.path = sockarr[1];
} else {
urlobj.host = u.host;
urlobj.port = u.port;
}
if (u.protocol === 'https:' && this.config.ignoretls) {
urlobj.requestCert = false;
urlobj.rejectUnauthorized = false;
} else if (u.protocol === 'https:') {
urlobj.cert = this.cert;
urlobj.key = this.key;
}
return urlobj;
};
function setOptsQuery(opts, options) {
if (options.query) {
let qstr;
let qchar = '?';
if (typeof options.query === 'object')
qstr = qs(options.query);
else
qstr = options.query;
if (opts.path.indexOf('?') > 0) qchar = '&';
opts.path += qchar + qstr;
}
}
gohttp.prototype.request = async function (url, options = null) {
let opts;
let is_obj = false;
if (typeof url === 'string') {
opts = this.parseUrl(url);
} else if (typeof url === 'object') {
opts = url;
is_obj = true;
} else {
throw new Error(`url must be a string or object`);
}
if (opts.timeout === undefined) {
opts.timeout = 35000;
}
if (options && !is_obj && typeof options === 'object' && opts !== options) {
for (let k in options) {
switch (k) {
case 'headers':
if (opts.headers) {
for(let i in options.headers) {
opts.headers[i] = options.headers[i];
}
} else {
opts.headers = options.headers;
}
break;
case 'query':
setOptsQuery(opts, options);
break;
default:
opts[k] = options[k];
}
}
}
let postData = {
'body': '',
'content-length': 0,
'content-type': ''
};
let postState = {
isUpload: false,
isPost: false
};
if (opts.method[0] === 'P' || (opts.method[0] === 'D' && (opts.body || opts.rawBody) ) )
{
//只有POST、PUT、PATCH的情况会出现参数错误。
if (opts.body === undefined && opts.rawBody === undefined) {
throw new Error('POST/PUT must with body data, please set body or rawBody');
}
if (opts.headers['content-type'] === undefined) {
opts.headers['content-type'] = 'application/x-www-form-urlencoded';
}
postState.isPost = true;
switch (opts.headers['content-type']) {
case 'application/x-www-form-urlencoded':
postData.body = Buffer.from(qs(opts.body));
break;
case 'multipart/form-data':
postState.isUpload = true;
postData = await this.bodymaker.makeUploadData(opts.body);
opts.headers['content-type'] = postData['content-type'];
break;
default:
if (opts.headers['content-type'].indexOf('multipart/form-data') >= 0) {
postState.isUpload = true;
if (options.rawBody !== undefined) {
postData = {
'content-type' : '',
'body' : options.rawBody,
'content-length' : options.rawBody.length
};
}
} else {
if (typeof opts.body === 'object') {
postData.body = Buffer.from(JSON.stringify(opts.body));
} else {
postData.body = Buffer.from(opts.body);
}
}
}
}
if (postState.isPost && !postState.isUpload) {
postData['content-type'] = opts.headers['content-type'];
postData['content-length'] = postData.body.length;
}
if (postState.isPost) {
opts.headers['content-length'] = postData['content-length'];
}
if (options && options.isDownload) {
return this._coreDownload(opts, postData, postState);
}
return this._coreRequest(opts, postData, postState);
};
gohttp.prototype._coreRequest = async function (opts, postData, postState) {
let h = (opts.protocol === 'https:') ? https : http;
let ret = {
buffers : [],
length: 0,
data : '',
ok : true,
status : 0,
timeout: false,
error: null,
headers : {},
};
ret.text = (ecd = 'utf8') => {
return ret.data.toString(ecd);
};
ret.json = (ecd = 'utf8') => {
return JSON.parse(ret.data.toString(ecd));
};
ret.blob = () => {
return ret.data;
};
return new Promise ((rv, rj) => {
let r = h.request(opts, (res) => {
if (opts.encoding) {
//默认为buffer
res.setEncoding(opts.encoding);
}
let bd = '';
let onData = (data) => {
//如果消息头有content-length则返回结果会是字符串而不是buffer。
//但是无法保证content-length和实际数据是否一致,所以会把字符串转换为buffer。
if (typeof data === 'string') {
bd = Buffer.from(data);
ret.buffers.push(bd);
ret.length += bd.length;
} else {
ret.buffers.push(data);
ret.length += data.length;
}
};
res.on('data', onData);
res.on('end', () => {
ret.data = Buffer.concat(ret.buffers, ret.length);
ret.buffers = null;
ret.status = res.statusCode;
ret.headers = res.headers;
if (res.statusCode >= 400) {
ret.ok = false;
} else {
ret.ok = true;
}
rv(ret);
});
res.on('error', (err) => {
ret.error = err;
rv(ret);
});
});
r.setTimeout(opts.timeout);
r.on('timeout', (sock) => {
r.destroy();
ret.timeout = true;
rv(ret);
});
r.on('error', (e) => { rj(e); });
if (postState.isPost) {
r.write(postData.body);
}
r.end();
});
};
gohttp.prototype._coreDownload = function (opts, postData, postState) {
let h = (opts.protocol === 'https:') ? https : http;
if (!opts.dir) {opts.dir = './';}
let getWriteStream = function (filename) {
if (opts.target) {
return fs.createWriteStream(opts.target, {encoding:'binary'});
} else {
let dfname = `${opts.dir}/${filename}`;
try {
fs.accessSync(dfname, fs.constants.F_OK);
dfname = `${Date.now()}-${dfname}`;
} catch(err) {}
return fs.createWriteStream(dfname,{encoding:'binary'});
}
};
let checkMakeFileName = function (filename = '') {
if (!filename) {
var nh = crypto.createHash('sha1');
nh.update(`${(new Date()).getTime()}--`);
filename = nh.digest('hex');
}
return filename;
};
let parseFileName = function (headers) {
let fname = '';
if(headers['content-disposition']) {
let name_split = headers['content-disposition'].split(';').filter(p => p.length > 0);
for (let i=0; i < name_split.length; i++) {
if (name_split[i].indexOf('filename*=') >= 0) {
fname = name_split[i].trim().substring(10);
//fname = fname.split('\'')[2];
//fname = decodeURIComponent(fname);
fname = decodeURIComponent( fname.split('\'')[2] );
} else if(name_split[i].indexOf('filename=') >= 0) {
fname = name_split[i].trim().substring(9);
}
}
}
return fname;
};
let downStream = null;
let filename = '';
let total_length = 0;
let sid = null;
let progressCount = 0;
let down_length = 0;
if (opts.progress === undefined) {
opts.progress = true;
}
return new Promise((rv, rj) => {
let r = h.request(opts, res => {
//res.setEncoding('binary');
filename = parseFileName(res.headers);
if (res.headers['content-length']) {
total_length = parseInt(res.headers['content-length']);
}
try {
filename = checkMakeFileName(filename);
downStream = getWriteStream(filename);
} catch (err) {
console.log(err);
res.destroy();
return ;
}
res.on('data', data => {
downStream.write(data);
down_length += data.length;
if (opts.progress && total_length > 0) {
if (down_length >= total_length) {
console.clear();
console.log('100.00%');
} else if (progressCount > 25) {
console.clear();
console.log(`${((down_length/total_length)*100).toFixed(2)}%`);
progressCount = 0;
}
}
});
res.on('end', () => {rv(true);});
res.on('error', (err) => { rj(err); });
sid = setInterval(() => {
progressCount+=1;
}, 20);
});
if (postState.isPost) {
r.write(postData.body, postState.isUpload ? 'binary' : 'utf8');
}
r.end();
})
.then((r) => {
if (opts.progress) { console.log('ok.'); }
}, (err) => {
throw err;
})
.catch(err => { throw err; })
.finally(() => {
if (downStream) {
downStream.end();
}
clearInterval(sid);
});
};
gohttp.prototype.checkMethod = function (method, options) {
if (typeof options !== 'object') {
options = {method: method};
} else if (!options.method || options.method !== method) {
options.method = method;
}
};
gohttp.prototype.get = async function (url, options = {}) {
this.checkMethod('GET', options);
return this.request(url, options);
};
gohttp.prototype.post = async function (url, options = {}) {
this.checkMethod('POST', options);
if (!options.body && !options.rawBody) {
throw new Error('must with body data');
}
return this.request(url, options);
};
gohttp.prototype.put = async function (url, options = {}) {
this.checkMethod('PUT', options);
if (!options.body && !options.rawBody) {
throw new Error('must with body data');
}
return this.request(url, options);
};
gohttp.prototype.patch = async function (url, options = {}) {
this.checkMethod('PATCH', options);
if (!options.body && !options.rawBody) {
throw new Error('must with body data');
}
return this.request(url, options);
};
gohttp.prototype.delete = async function (url, options = {}) {
this.checkMethod('DELETE', options);
return this.request(url, options);
};
gohttp.prototype.options = async function (url, options = {}) {
this.checkMethod('OPTIONS', options);
return this.request(url, options);
};
gohttp.prototype.upload = async function (url, options = {}) {
if (typeof options !== 'object') {
options = {method: 'POST'};
}
if (options.method === undefined) {
options.method = 'POST';
}
if (options.method[0] !== 'P' && options.method[0] !== 'D') {
throw new Error('必须是POST、PUT、PATCH、DELETE请求之一。');
}
if (!options.files && !options.form && !options.body && !options.rawBody) {
throw new Error('没有请求体数据(file or form not found.)');
}
//没有设置body,但是存在files或form,则自动打包成request需要的格式。
if (!options.body && !options.rawBody) {
options.body = {};
if (options.files) {
options.body.files = options.files;
delete options.files;
}
if (options.form) {
options.body.form = options.form;
delete options.form;
}
}
if (!options.headers) {
options.headers = {
'content-type' : 'multipart/form-data'
};
}
if (!options.headers['content-type']
|| options.headers['content-type'].indexOf('multipart/form-data') < 0)
{
options.headers['content-type'] = 'multipart/form-data';
}
return this.request(url, options);
};
gohttp.prototype.download = function(url, options = {}) {
if (typeof options !== 'object') {
options = {
method: 'GET',
isDownload: true
};
} else {
if (!options.isDownload) {options.isDownload = true; }
}
return this.request(url, options);
};
//upload的简单封装,让参数更简单,用于快速文件上传写起来简单些。
gohttp.prototype.up = async function (url, opts = {}) {
if (typeof opts !== 'object' || opts.file === undefined) {
throw new Error('Error: file or form not found. options : {file:FILE_PATH, name : UPLOAD_NAME}');
}
/* if (opts.name === undefined) {
opts.name = 'file';
} */
opts.files = {};
opts.files[ opts.name || 'file' ] = opts.file;
return this.upload(url, opts);
};
/**
* 这个接口主要是为了快速转发,接收到的数据,不需要经过任何解析,直接转发,不经过request接口的复杂选项解析。
* 并且body必须是buffer类型。 如果确定了要转发的url,你可以先通过parseUrl解析后并保存结果,之后每次都直接传递这个对象。
*/
gohttp.prototype.transmit = function (url, opts = {}) {
let postopts = {
isPost: false
};
if (opts.rawbody && opts.rawbody instanceof Buffer) {
postopts.isPost = true;
} else {
opts.rawbody = '';
}
let uobj = null;
if (typeof url === 'string') {
uobj = this.parseUrl(url);
} else if (url && typeof url === 'object') {
uobj = url;
} else {
throw new Error('url must be string or a object');
}
if (opts.headers && typeof opts.headers === 'object') {
for (let k in opts.headers) {
uobj.headers[k] = opts.headers[k];
}
}
if (opts.timeout && typeof opts.timeout == 'number') {
uobj.timeout = opts.timeout;
} else {
uobj.timeout = 35000;
}
if (opts.method) {
uobj.method = opts.method;
}
return this._coreRequest(uobj, {body: opts.rawbody}, postopts);
};
/** ------------ 兼容http2的接口层,和hiio.js接口一致 ---------------- */
let compatMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
function _hiicompat(url, options, t) {
if (!(this instanceof _hiicompat)) {
return new _hiicompat(url, options, t);
}
this.url = url;
this.req = t;
this.request = t.request;
this.urlobj = this.req.parseUrl(url);
this.host = this.urlobj.host;
this.options = options;
this.port = this.urlobj.port;
this.methods = compatMethods;
this.headers = null;
options.headers && this.setHeader(options.headers)
Object.defineProperty(this, '__prefix__', {
configurable: false,
writable: true,
enumerable: false,
value: fmtpath(this.urlobj.pathname)
});
};
compatMethods.forEach(m => {
let mlower = m.toLowerCase();
_hiicompat.prototype[ mlower ] = function (opts) {
this.setOptions(opts, m, true);
return this.req.request(opts);
};
});
/**
*
* @param {object} opts {path: '/', headers:{...}}
*/
_hiicompat.prototype.upload = function (opts) {
//在request中,检测到如果两个参数相同则不会把options中的值复制给opts
//使用这种方式,可以利用upload的选项检测操作。
this.setOptions(opts, 'POST');
return this.req.upload(opts, opts);
};
_hiicompat.prototype.up = function (opts) {
this.setOptions(opts, 'POST');
return this.req.up(opts, opts);
};
_hiicompat.prototype.download = function (opts) {
this.setOptions(opts, 'GET');
return this.req.download(opts, {isDownload: true});
};
_hiicompat.prototype.copyUrl = function () {
let new_url = {
...this.urlobj
};
new_url.headers = {};
return new_url;
};
_hiicompat.prototype.setHeader = function (key, val = null) {
if (!this.headers) this.headers = Object.create(null);
if (typeof key === 'object') {
for (let k in key) this.headers[k] = key[k];
} else {
this.headers[key] = val;
}
return this;
};
Object.defineProperty(_hiicompat.prototype, 'prefix', {
get: function () {
return this.__prefix__;
},
set: function (path) {
this.__prefix__ = fmtpath(path);
}
});
Object.defineProperty(_hiicompat.prototype, 'setOptions', {
value: setOptions,
configurable: false,
writable: false,
enumerable: false
});
function setOptions(opts, method, resetMethod = false) {
for (let k in this.urlobj) {
if (k === 'headers' || k === 'method' || k === 'path')
continue;
opts[k] = this.urlobj[k];
}
if (!opts.method || resetMethod) opts.method = method;
if (!opts.headers) opts.headers = {};
if (this.headers && typeof this.headers === 'object') {
for (let k in this.headers) {
opts.headers[k] = this.headers[k];
}
}
let options = this.options;
if (options) {
if (options.headers) {
for (let k in options.headers)
opts.headers[k] = options.headers[k];
}
for (let k in options) {
if (k === 'headers') continue;
opts[k] = options[k];
}
}
if (opts.timeout === undefined && options.timeout)
opts.timeout = options.timeout;
if (!opts.path) opts.path = this.urlobj.path;
else {
if (this.__prefix__ !== '' && !opts.withoutPrefix) {
opts.path = `${this.__prefix__}${opts.path}`;
}
}
if (opts.query) setOptsQuery(opts, opts);
};
/**
* 返回 _hiicompat 实例。为兼容hiio而提供。
* @param {string} url url字符串
*/
gohttp.prototype.connect = function (url, options = null) {
if (typeof options !== 'object') options = null;
return new _hiicompat(url, options || {}, this);
};
/** ------------------------------END------------------------------ */
module.exports = gohttp;