whistle
Version:
HTTP, HTTPS, Websocket debugging proxy
647 lines (570 loc) • 20.3 kB
JavaScript
var https = require('https');
var http = require('http');
var net = require('net');
var url = require('url');
var mime = require('mime');
var extend = require('util')._extend;
var util = require('../util');
var properties = require('../rules/util').properties;
var WhistleTransform = util.WhistleTransform;
var SpeedTransform = util.SpeedTransform;
var ReplacePatternTransform = util.ReplacePatternTransform;
var ReplaceStringTransform = util.ReplaceStringTransform;
var FileWriterTransform = util.FileWriterTransform;
var rules = require('../rules');
var pluginMgr = require('../plugins');
var LOCALHOST = '127.0.0.1';
var SCRIPT_START = util.toBuffer('<script>');
var SCRIPT_END = util.toBuffer('</script>');
var STYLE_START = util.toBuffer('<style>');
var STYLE_END = util.toBuffer('</style>');
var REQ_HEADER_RE = /^req\.headers\.(.+)$/;
var RES_HEADER_RE = /^res\.headers\.(.+)$/;
var HEADER_RE = /^headers\.(.+)$/;
function getDeleteProperties(deleteRule) {
var reqHeaders = {};
var resHeaders = {};
Object.keys(deleteRule).forEach(function(prop) {
if (REQ_HEADER_RE.test(prop)) {
reqHeaders[RegExp.$1.toLowerCase()] = 1;
} else if (RES_HEADER_RE.test(prop)) {
resHeaders[RegExp.$1.toLowerCase()] = 1;
} else if (HEADER_RE.test(prop)) {
reqHeaders[RegExp.$1.toLowerCase()] = 1;
resHeaders[RegExp.$1.toLowerCase()] = 1;
}
});
return {
reqHeaders: reqHeaders,
resHeaders: resHeaders
};
}
function handleCookies(data, cookies) {
var list = cookies && Object.keys(cookies);
var curCookies = data.headers && data.headers['set-cookie'];
if (!Array.isArray(curCookies)) {
curCookies = curCookies ? [curCookies + ''] : [];
}
if (list && list.length) {
var result = {};
curCookies.forEach(function(cookie) {
var index = cookie.indexOf('=');
if (index == -1) {
result[cookie] = null;
} else {
result[cookie.substring(0, index)] = cookie.substring(index + 1);
}
});
list.forEach(function(name) {
var cookie = cookies[name];
name = util.encodeURIComponent(name);
if (!cookie || typeof cookie != 'object') {
result[name] = cookie ? util.encodeURIComponent(cookie) : cookie;
} else {
var attrs = [];
var value = cookie.value;
attrs.push(value ? util.encodeURIComponent(value) : (value == null ? '' : value));
var maxAge = cookie.maxAge && parseInt(cookie.maxAge, 10);
if (!Number.isNaN(maxAge)) {
attrs.push('Expires=' + new Date(Date.now() + maxAge * 1000).toGMTString());
attrs.push('Max-Age=' + maxAge);
}
cookie.secure && attrs.push('Secure');
cookie.path && attrs.push('Path=' + cookie.path);
cookie.domain && attrs.push('Domain=' + cookie.domain);
cookie.httpOnly && attrs.push('HttpOnly');
result[name] = attrs.join('; ');
}
});
cookies = Object.keys(result).map(function(name) {
var value = result[name];
return name + (value == null ? '' : '=' + value);
});
util.setHeader(data, 'set-cookie', cookies);
}
}
function handleCors(data, cors) {
if (!cors) {
return;
}
if (cors.origin !== undefined) {
util.setHeader(data, 'access-control-allow-origin', cors.origin);
}
if (cors.methods !== undefined) {
util.setHeader(data, 'access-control-allow-methods', cors.methods);
}
if (cors.headers !== undefined) {
util.setHeader(data, 'access-control-expose-headers', cors.headers);
}
if (cors.credentials !== undefined) {
util.setHeader(data, 'access-control-allow-credentials', cors.credentials);
}
if (cors.maxAge !== undefined) {
util.setHeader(data, 'access-control-max-age', cors.maxAge);
}
}
function setCookies(headers, data) {
var newCookies = data.headers['set-cookie'];
if (newCookies) {
var cookies = headers['set-cookie'];
if (Array.isArray(cookies)) {
var newNameMap = {};
newCookies.forEach(function(cookie) {
var index = cookie.indexOf('=');
var name = index == -1 ? name : cookie.substring(0, index);
newNameMap[name] = 1;
});
cookies.forEach(function(cookie) {
var index = cookie.indexOf('=');
var name = index == -1 ? name : cookie.substring(0, index);
if (!newNameMap[name]) {
newCookies.push(cookie);
}
});
}
headers['set-cookie'] = newCookies;
delete data.headers['set-cookie'];
}
}
function removeDisableProps(req, headers) {
var disable = req.disable;
if (disable.cookie || disable.cookies || disable.resCookie || disable.resCookies) {
delete headers['set-cookie'];
}
if (disable.cache) {
headers['cache-control'] = 'no-cache';
headers.expires = new Date(Date.now() -60000000).toGMTString();
headers.pragma = 'no-cache';
}
disable.csp && util.disableCSP(headers);
}
function handleReplace(res, replacement) {
if (!replacement) {
return;
}
var type = util.getContentType(res.headers);
if (!type || type == 'IMG') {
return;
}
Object.keys(replacement).forEach(function(pattern) {
var value = replacement[pattern];
if (util.isOriginalRegExp(pattern) && (pattern = util.toOriginalRegExp(pattern))) {
res.addTextTransform(new ReplacePatternTransform(pattern, value));
} else if (pattern) {
res.addTextTransform(new ReplaceStringTransform(pattern, value));
}
});
}
function getWriterFile(file, statusCode) {
if (!file || statusCode == 200) {
return file;
}
return file + '.' + statusCode;
}
module.exports = function(req, res, next) {
var config = this.config;
var responsed, timer;
var resRules = req.rules;
var deleteHeaders = getDeleteProperties(req['delete']);
req.request = function(options) {
options = options || req.options;
req.realUrl = res.realUrl = options.href;
var now = Date.now();
rules.getProxy(options.href, options.isPlugin ? null : req, function(err, hostIp, hostPort) {
var proxyUrl = !options.isPlugin && resRules.proxy ? resRules.proxy.matcher : null;
var headers = req.headers;
var curUrl;
if (!hostIp) {
if (options.localDNS && net.isIP(options.host)) {
curUrl = options.host;
} else if (proxyUrl) {
var isInternalProxy = /^internal-proxy:\/\//.test(proxyUrl);
if (isInternalProxy || /^https2http-proxy:\/\//.test(proxyUrl)) {
if (isInternalProxy && options.protocol === 'https:') {
headers[config.HTTPS_FIELD] = 1;
}
options.protocol = null;
} else if (/^http2https-proxy:\/\//.test(proxyUrl)) {
options.protocol = 'https:';
}
curUrl = 'http:' + util.removeProtocol(proxyUrl);
} else {
curUrl = options.href;
}
}
rules.resolveHost(curUrl, function(err, ip, port, host) {
ip = hostIp || ip;
port = hostPort || port;
req.dnsTime = Date.now() - now;
req.hostIp = ip;
if (host) {
resRules.host = host;
}
if (err) {
res.response(util.wrapGatewayError('DNS Lookup Failed\r\n' + util.getErrorStack(err)));
return;
}
var isHttps = options.protocol == 'https:';
var proxyOptions, isProxyPort;
if (proxyUrl) {
proxyOptions = url.parse(proxyUrl);
proxyOptions.host = ip;
var isSocks = proxyOptions.protocol == 'socks:';
if (isHttps || isSocks) {
isProxyPort = (proxyOptions.port || (isSocks ? 1080 : 80)) == config.port;
if (isProxyPort && util.isLocalAddress(ip)) {
res.response(util.wrapResponse({
statusCode: 302,
headers: {
location: 'http://' + ip + ':' + config.port + (options.path || '')
}
}));
} else {
var proxyHeaders = {
host: options.hostname + ':' + (isHttps ? (options.port || 443) : 80),
'proxy-connection': 'keep-alive',
'user-agent': headers['user-agent'] || 'whistle/' + config.version
};
if (isHttps && isProxyPort) {
proxyHeaders[config.WEBUI_HEAD] = 1;
}
var opts = {
isHttps: isHttps,
host: ip,
port: proxyOptions.port,
url: options.href,
auth: proxyOptions.auth,
headers: proxyHeaders
};
options.agent = isSocks ? config.getSocksAgent(opts) : config.getHttpsAgent(opts);
request(options);
}
return;
}
if (proxyOptions.auth) {
headers['proxy-authorization'] = 'Basic ' + util.toBuffer(proxyOptions.auth + '').toString('base64');
}
}
req.hostIp = port ? ip + ':' + port : ip;
port = proxyOptions ? (proxyOptions.port || 80) : (port || options.port);
options.host = ip;//设置ip
isProxyPort = (port || (isHttps ? 443 : 80)) == config.port;
if (isProxyPort && util.isLocalAddress(options.host)) {
res.response(util.wrapResponse({
statusCode: 302,
headers: {
location: 'http://' + ip + ':' + config.port + (options.path || '')
}
}));
} else {
if (isProxyPort) {
headers[config.WEBUI_HEAD] = 1;
}
options.agent = req.disable.keepAlive ? false : (isHttps ? config.httpsAgent : config.httpAgent);
request(options, port, true);
}
function request(options, port, isDirectReq) {
options.headers = headers;
options.method = req.method;
options.rejectUnauthorized = false;
if (!options.isPlugin) {
headers.host = options.hostname + (options.port ? ':' + options.port : '');
}
if (isDirectReq) {
options.port = port;
}
if (proxyUrl && isDirectReq) {
options.path = options.href;
}
if (resRules.hostname) {
headers.host = util.getMatcherValue(resRules.hostname);
}
delete options.hostname; //防止自动dns
var _request = function() {
try {
var client = (isHttps ? https : http).request(options, res.response);
clearTimeout(timer);
if (!timer && !util.hasRequestBody(req)) {
client.once('finish', function() {
timer = setTimeout(function() {
if (responsed) {
return;
}
client.abort();
}, config.responseTimeout);
});
}
return req.pipe(client);
} catch(e) {
res.response(util.wrapGatewayError(util.getErrorStack(e)));
}
return req;
};
Object.keys(deleteHeaders.reqHeaders).forEach(function(prop) {
delete req.headers[prop];
});
options.headers = util.formatHeaders(options.headers, req.rawHeaderNames);
_request().on('error', function(err) {
if (responsed) {
return;
}
if (util.hasRequestBody(req)) {
res.response(util.wrapGatewayError(util.getErrorStack(err)));
} else {
_request().on('error', function(err) {
if (!responsed) {
_request().on('error', function(err) {
res.response(util.wrapGatewayError(util.getErrorStack(err)));
});
}
});
}
});
}
}, req.pluginRules, req.rulesFileMgr);
});
};
res.response = function(_res) {
if (responsed) {
return;
}
clearTimeout(timer);
responsed = true;
if (_res.realUrl) {
req.realUrl = res.realUrl = _res.realUrl;
}
res.headers = _res.headers;
res.trailers = _res.trailers;
if (_res.rawHeaderNames) {
res.rawHeaderNames = _res.rawHeaderNames;
} else {
res.rawHeaderNames = _res.rawHeaderNames = Array.isArray(_res.rawHeaders) ?
util.getRawHeaderNames(_res.rawHeaders) : {};
}
pluginMgr.getResRules(req, _res, function(resRulesMgr) {
util.mergeRules(req, resRulesMgr && resRulesMgr.resolveRules(req.fullUrl));
var replaceStatus = util.getMatcherValue(resRules.replaceStatus);
if (replaceStatus && replaceStatus != _res.statusCode) {
res.statusCode = _res.statusCode = replaceStatus;
util.handleStatusCode(replaceStatus, _res.headers);
}
if (req.disable['301'] && _res.statusCode == 301) {
res.statusCode = _res.statusCode = 302;
}
var cors = resRules.resCors;
var resCors = util.getMatcherValue(cors);
if (resCors == 'use-credentials') {
cors = null;
resCors = 'enable';
} else if (resCors == '*' || resCors == 'enable') {
cors = null;
} else {
resCors = null;
}
util.parseRuleJson([resRules.res, resRules.resHeaders, resRules.resCookies, cors, resRules.resReplace],
function(data, headers, cookies, cors, replacement) {
if (resRules.head && resRules.head.res) {
data = extend(resRules.head.res, data);
}
data = data || {};
if (headers) {
data.headers = extend(data.headers || {}, headers);
}
if (data.headers) {
data.headers = util.lowerCaseify(data.headers, res.rawHeaderNames);
}
handleCookies(data, cookies);
if (resCors == '*') {
util.setHeader(data, 'access-control-allow-origin', '*');
} else if (resCors == 'enable') {
var origin = req.headers.origin;
util.setHeaders(data, {
'access-control-allow-credentials': !!origin,
'access-control-allow-origin': origin || '*'
});
} else {
handleCors(data, cors);
}
var cache = util.getMatcherValue(resRules.cache);
var maxAge = parseInt(cache, 10);
var noCache = /^(?:no|no-cache|no-store)$/i.test(cache) || maxAge < 0;
if (maxAge > 0 || noCache) {
util.setHeaders(data, {
'cache-control': noCache ? (/^no-store$/i.test(cache) ? 'no-store' : 'no-cache') : 'max-age=' + maxAge,
'expires': new Date(Date.now() + (noCache ? -60000000 : maxAge * 1000)).toGMTString(),
'pragma': noCache ? 'no-cache' : ''
});
}
if (resRules.attachment) {
var attachment = util.getMatcherValue(resRules.attachment) || util.getFilename(req.fullUrl);
util.setHeader(data, 'content-disposition', 'attachment; filename="' + attachment + '"');
util.disableReqCache(req.headers);
}
if (resRules.location) {
util.setHeader(data, 'location', util.getMatcherValue(resRules.location));
}
if (resRules.resType) {
var newType = util.getMatcherValue(resRules.resType).split(';');
var type = newType[0];
newType[0] = (!type || type.indexOf('/') != -1) ? type : mime.lookup(type, type);
util.setHeader(data, 'content-type', newType.join(';'));
}
if (resRules.resCharset) {
data.charset = util.getMatcherValue(resRules.resCharset);
}
var resDelay = util.getMatcherValue(resRules.resDelay);
resDelay = resDelay && parseInt(resDelay, 10);
if (resDelay > 0) {
data.delay = resDelay;
}
var resSpeed = util.getMatcherValue(resRules.resSpeed);
resSpeed = resSpeed && parseFloat(resSpeed);
if (resSpeed > 0) {
data.speed = resSpeed;
}
util.readInjectFiles(data, function(data) {
util.getRuleValue([resRules.resBody, resRules.resPrepend, resRules.resAppend, resRules.html, resRules.js, resRules.css],
function(resBody, resPrepend, resAppend, html, js, css) {
if (resBody) {
data.body = resBody;
}
if (resPrepend) {
data.top = resPrepend;
}
if (resAppend) {
data.bottom = util.toBuffer(resAppend);
}
var headers = _res.headers;
var type;
if (data.headers) {
setCookies(headers, data);
type = data.headers['content-type'];
if (typeof type == 'string') {
if (type.indexOf(';') == -1) {
var origType = headers['content-type'];
if (typeof origType == 'string' && origType.indexOf(';') != -1) {
origType = origType.split(';');
origType[0] = type;
data.headers['content-type'] = origType.join(';');
}
}
} else {
delete data.headers['content-type'];
}
extend(headers, data.headers);
}
var charset;
if (typeof data.charset == 'string') {
type = headers['content-type'];
charset = '; charset=' + data.charset;
if (typeof type == 'string') {
headers['content-type'] = type.split(';')[0] + charset;
} else {
headers['content-type'] = charset;
}
} else {
delete data.charset;
}
if (!headers.pragma) {
delete headers.pragma;
}
if (headers.location) {
//nodejs的url只支持ascii,对非ascii的字符要encodeURIComponent,否则传到浏览器是乱码
headers.location = util.encodeNonAsciiChar(headers.location);
}
var speedTransform = data.speed || data.delay ? new SpeedTransform(data) : null;
delete data.headers;
delete data.speed;
delete data.delay;
type = util.getContentType(headers);
charset = util.getCharset(headers['content-type']);
var bottom;
switch(type) {
case 'HTML':
if (css) {
bottom = Buffer.concat([STYLE_START, util.toBuffer(css, charset), STYLE_END]);
}
if (html) {
html = util.toBuffer(html, charset);
bottom = bottom ? Buffer.concat([bottom, html]) : html;
}
if (js) {
js = Buffer.concat([SCRIPT_START, util.toBuffer(js, charset), SCRIPT_END]);
bottom = bottom ? Buffer.concat([bottom, js]) : js;
}
break;
case 'JS':
bottom = js;
break;
case 'CSS':
bottom = css;
break;
}
if (bottom) {
bottom = util.toBuffer(bottom, charset);
data.bottom = data.bottom ? Buffer.concat([data.bottom, bottom]) : bottom;
}
if (!util.isEmptyObject(data)) {
res.addZipTransform(new WhistleTransform(data));
}
if (util.hasBody(_res)) {
handleReplace(res, replacement);
}
//一定放在最后,确保能过滤到动态注入的内容
if (speedTransform) {
res.add(speedTransform);
}
util.drain(req, function() {
util.getFileWriters([util.hasBody(_res) ? getWriterFile(util.getRuleFile(resRules.resWrite), _res.statusCode) : null,
getWriterFile(util.getRuleFile(resRules.resWriteRaw), _res.statusCode)], function(writer, rawWriter) {
res.on('src', function(_res) {
if (writer) {
res.addZipTransform(new FileWriterTransform(writer, _res));
}
if (rawWriter) {
res.addZipTransform(new FileWriterTransform(rawWriter, _res, true, req));
}
});
res.src(_res);
removeDisableProps(req, _res.headers);
if (properties.get('showHostIpInResHeaders')) {
_res.headers['x-host-ip'] = req.hostIp || LOCALHOST;
}
var resHeaders = getDeleteProperties(req['delete']).resHeaders;
Object.keys(resHeaders).forEach(function(prop) {
delete _res.headers[prop];
});
try {
res.writeHead(_res.statusCode, util.formatHeaders(_res.headers, res.rawHeaderNames));
_res.trailers && res.addTrailers(_res.trailers);
} catch(e) {
e._resError = true;
util.emitError(res, e);
}
});
});
}, !util.hasBody(_res));
});
});
});
};
var statusCode = util.getMatcherValue(resRules.statusCode);
var resHeaders = {};
if (!statusCode && resRules.redirect) {
statusCode = 302;
resHeaders.location = util.getMatcherValue(resRules.redirect);
}
if (statusCode) {
req.hostIp = LOCALHOST;
res.response(util.wrapResponse({
statusCode: statusCode,
headers: util.handleStatusCode(statusCode, resHeaders)
}));
return;
}
if (resRules.attachment || resRules.resReplace || resRules.resBody || resRules.resPrepend || resRules.resAppend
|| resRules.html || resRules.js || resRules.css || resRules.resWrite || resRules.resWriteRaw) {
util.disableReqCache(req.headers);
}
next();
};