koa-web-kit
Version:
A modern, production-ready, and full-stack node web framework
300 lines (275 loc) • 8.05 kB
JavaScript
/**
* Proxy/Send requests to backend or other endpoints
*/
const got = require('got');
const tunnel = require('tunnel');
const SocksProxyAgent = require('socks-proxy-agent');
const { URL } = require('url');
const util = require('util');
const { logger } = require('./logger');
const appConfig = require('../config/env');
const { HTTP_METHOD } = require('./http-config');
const isCustomAPIPrefix = appConfig.isCustomAPIPrefix();
const defaultEndpoint = appConfig.getDefaultApiEndPoint();
const httpProxy = appConfig.getHttpProxy();
const socksProxy = appConfig.getSocksProxy();
const debugLevel = appConfig.getProxyDebugLevel();
const DEBUG_LEVEL = {
NONE: 0,
PLAIN: 1,
VERBOSE: 2,
};
const LOG_LEVEL = {
INFO: 'info',
WARN: 'warn',
ERROR: 'error',
};
/**
* proxy global default got options
*/
const defaultRequestOptions = {
throwHttpErrors: false,
};
/**
* Proxy for apis or other http requests
*/
class HttpClient {
/**
*
* @param {object=} options - Proxy instance options
* {
* endPoint: string, //endpoint for the instance, e.g: http://domain.com
* prefix: string, //extra prefix for url path
* debugLevel: number, //default based on the global app config
* }
* @param {object=} requestOptions - default options for "got" module
*/
constructor(options = {}, requestOptions = {}) {
this.endPoint = options.endPoint || defaultEndpoint;
this.endPointHost = '';
this.endPointParsedUrl = {};
if (this.endPoint) {
this.endPointParsedUrl = new URL(this.endPoint);
this.endPointHost = this.endPointParsedUrl.host;
}
const clientBaseOptions = {
baseUrl: this.endPoint,
};
const agent = this._getAgent();
if (agent) {
clientBaseOptions.agent = agent;
}
this.options = options;
this.got = got.extend(
Object.assign(clientBaseOptions, defaultRequestOptions, requestOptions)
);
this.debugLevel = options.debugLevel || debugLevel;
}
/**
* Get the http agent for proxying or requesting,
* NOTE: when use https endpoint, you may need to set "NODE_TLS_REJECT_UNAUTHORIZED=0" node option
* with self signed certificate to bypass the TLS check
* @return {*}
* @private
*/
_getAgent() {
let agent;
//Simple proxy tunnel
if (socksProxy) {
const socksProtocol = 'socks:';
agent = new SocksProxyAgent(
String(socksProxy).startsWith(socksProtocol)
? socksProxy
: `${socksProtocol}//${socksProxy}`
);
} else if (httpProxy) {
const parsedUrl = new URL(httpProxy);
const tunnelOptions = {
proxy: {
host: parsedUrl.hostname,
port: parsedUrl.port,
},
};
agent =
this.endPointParsedUrl.protocol === 'https:'
? tunnel.httpsOverHttp(tunnelOptions)
: tunnel.httpOverHttp(tunnelOptions);
}
return agent;
}
/**
* Customize real options for destination
* @param ctx
* @param options
* @return {object}
*/
_prepareRequestOptions(ctx, options = { headers: {} }) {
options.url = ctx.url;
options.method = ctx.method;
options.headers = Object.assign({}, ctx.headers, options.headers);
return this._finalizeRequestOptions(options);
}
/**
* Proxy koa http request to another endpoint
* @param ctx - koa ctx
* @param options - custom request options
* @return {Stream}
*/
proxyRequest(ctx, options = {}) {
let requestStream;
const opts = this._prepareRequestOptions(ctx, options);
// console.log('opts: ', opts);
requestStream = ctx.req.pipe(this.got.stream(opts.url, opts));
if (this.debugLevel) {
this.handleProxyEvents(requestStream);
}
requestStream.on('error', (error, body) => {
this._log(null, error, LOG_LEVEL.ERROR);
this._log(`response body: ${JSON.stringify(body)}`);
});
return requestStream;
}
handleProxyEvents(requestStream) {
let chunks = [];
let gotOptions = {};
let gotResponse;
requestStream.on('response', response => {
gotResponse = response;
const request = response.request;
if (request) {
gotOptions = request.gotOptions;
this._log(
`[${response.url}] request options: \n${util.inspect(
request.gotOptions
)}`
);
}
this._log(
`[${response.url}] response headers: \n${util.inspect(
response.headers
)}`
);
});
if (this.debugLevel > DEBUG_LEVEL.PLAIN) {
requestStream.on('data', chunk => {
chunks.push(chunk);
});
requestStream.on('end', () => {
const ret = Buffer.concat(chunks);
const type = gotResponse.headers['content-type'];
if (this._isPlainTextBody(type)) {
this._log(
`[${gotOptions.method}][${
gotOptions.href
}] response body: ${ret.toString()}`
);
} else {
this._log(
`[${gotOptions.method}][${gotOptions.href}] response body[${type}] length: ${ret.length}`
);
}
});
}
}
/**
* Send http requests
* @param {string|object} url - request url in string or object which has a "url" property
* @param {object=} options - custom request options
* @return {Promise<any>}
*/
async sendRequest(url, options = {}) {
if ('string' === typeof url) {
options.url = url;
}
const opts = this._finalizeRequestOptions(options);
// console.log('opts: ', opts);
let ret = {};
try {
const response = await this.got(opts.url, opts);
ret = response.body;
} catch (err) {
this._log(null, err, LOG_LEVEL.ERROR);
return Promise.reject(err);
}
return Promise.resolve(ret);
}
/**
* Proxy logger
* @param {*=} msg - log message
* @param {Error=} error - the Error instance
* @param {string=} level - log level
*/
_log(msg, error, level = LOG_LEVEL.INFO) {
msg && logger[level](msg);
error && logger[level](error.stack);
}
/**
* Throw an exception
* @param msg
*/
_exception(msg) {
throw new Error(msg);
}
get(url, query, options = {}) {
options.method = HTTP_METHOD.GET;
if (query) {
options.query = query;
}
return this.sendRequest(url, options);
}
post(url, body, options = {}) {
return this.sendRequestWithBody(url, body, HTTP_METHOD.POST, options);
}
put(url, body, options = {}) {
return this.sendRequestWithBody(url, body, HTTP_METHOD.PUT, options);
}
patch(url, body, options = {}) {
return this.sendRequestWithBody(url, body, HTTP_METHOD.PATCH, options);
}
delete(url, options = {}) {
options.method = HTTP_METHOD.DELETE;
return this.sendRequest(url, options);
}
sendRequestWithBody(url, body, method, options = {}) {
this._normalizeBodyContentType(body, options);
options.method = method;
return this.sendRequest(url, options);
}
/**
* Normalize body for the upcoming request
* @param data - body data
* @param options - to which the body data will be attached
* @return {object}
*/
_normalizeBodyContentType(data, options = {}) {
options.body = data;
return options;
}
/**
* Get final request options
* @param options - custom options
* @return {object} final request options
*/
_finalizeRequestOptions(options = {}) {
let optionPrefix = options.prefix || this.options.prefix;
// TODO: a path rewrite could be better
if (isCustomAPIPrefix && optionPrefix) {
options.url = options.url.replace(new RegExp(`^${optionPrefix}`), '');
}
if (!options.headers) {
options.headers = {};
}
if (this.endPointHost) {
options.headers.host = this.endPointHost;
}
return options;
}
_isPlainTextBody(contentType) {
if (!contentType) return true;
return !!(
contentType.startsWith('text/') ||
contentType.startsWith('application/json')
);
}
}
exports.HttpClient = HttpClient;