whistle
Version:
HTTP, HTTP2, HTTPS, Websocket debugging proxy
383 lines (362 loc) • 11 kB
JavaScript
var fs = require('fs');
var extend = require('extend');
var util = require('../util');
var mime = require('mime');
var qs = require('querystring');
var rulesMgr = require('../rules');
var protoMgr = require('../rules/protocols');
var pluginMgr = require('../plugins');
var CRLF_RE = /\r\n|\r|\n/g;
var RAW_FILE_RE = /rawfile/;
var HEADERS_SEP_RE = /(\r?\n(?:\r\n|\r|\n)|\r\r\n?)/;
var MAX_HEADERS_SIZE = 256 * 1024;
var TPL_RE = /(?:dust|tpl|jsonp):$/;
var VAR_RE = /\{\S+\}/;
var QUERY_VAR_RE = /\$?(?:\{\{([\w$-]+)\}\}|\{([\w$-]+)\})/g;
var UTF8_OPTIONS = { encoding: 'utf8' };
var ENABLE_OPTIONS = { enable: true };
var SLASH_RE = /\\+/g;
var QUOTE_RE = /"/g;
var JS_RE = /^js:/i;
var HTML_RE = /^html:/i;
var REPLACE_RE = /^replace:/i;
var BLANK_RE = /\s/g;
var HASH_RE = /#.*$/;
var CROSS_RE = /^x(s)?/;
var HTTP_RE = /^http:/;
var NOT_A_FILE_ERR = new Error('Not a file');
var INVALID_PATH = '(Path contains parent directory notation \'..\')';
function urlToStr(url) {
return url.replace(SLASH_RE, '').replace(QUOTE_RE, '\\$&').replace(BLANK_RE, ' ');
}
function isRawFileProtocol(protocol) {
return RAW_FILE_RE.test(protocol);
}
function readFiles(files, callback) {
var file = files.shift();
var execCallback = function (err, stat) {
if (!err && stat && stat.isFile()) {
callback(null, file, stat.size);
} else if (file && file.originalKey) {
pluginMgr.requestBin(file, function(buffer, err, res) {
file = file.originalKey;
callback(err, file, buffer ? buffer.length : 0, buffer, res);
});
} else if (files.length) {
readFiles(files, callback);
} else {
callback(err || NOT_A_FILE_ERR, file == null ? INVALID_PATH : file);
}
};
!file || typeof file != 'string'
? execCallback()
: util.getStat(file, execCallback);
}
function parseRes(str, rawHeaderNames, fromValue) {
if (!str) {
return {
statusCode: 200,
headers: {}
};
}
var headers = str.split(CRLF_RE);
var statusLine = headers.shift().trim().split(/\s+/g);
headers = util.parseHeaders(headers, rawHeaderNames);
if (fromValue) {
delete headers['content-encoding'];
}
return {
statusCode: statusLine[1] || 200,
headers: headers
};
}
function addLength(reader, length) {
reader.headers['content-length'] = length;
}
function getRawResByValue(body) {
var headers;
if (HEADERS_SEP_RE.test(body)) {
var crlfStr = RegExp.$1;
var index = body.indexOf(crlfStr);
headers = body.substring(0, index);
body = body.substring(index + crlfStr.length);
}
var rawHeaderNames = {};
var reader = parseRes(headers, rawHeaderNames, true);
reader.rawHeaderNames = rawHeaderNames;
reader.body = body;
addLength(reader, body ? Buffer.byteLength(body) : 0);
return reader;
}
function getRawResByPath(protocol, path, req, size, callback, body) {
var isRawFile = isRawFileProtocol(protocol);
var range = isRawFile ? undefined : util.parseRange(req, req.localFileSize);
var reader;
if (body == null) {
reader = fs.createReadStream(path, range);
} else {
reader = util.createTransform();
if (!body || !range) {
reader.end(body);
} else {
reader.end(body.slice(range.start, range.end + 1));
}
}
var rawHeaderNames = (reader.rawHeaderNames = rawHeaderNames);
if (isRawFile) {
var buffer;
var response = function (err, crlf) {
reader.removeAllListeners();
reader.on('error', util.noop);
var stream = reader;
reader = util.createTransform();
if (err) {
var stack = util.getErrorStack(err);
reader.statusCode = 500;
reader.push(stack);
reader.push(null);
size = Buffer.byteLength(stack);
} else {
if (crlf) {
crlf = util.toBuffer(crlf);
var index = buffer.indexOf(crlf);
if (index != -1) {
extend(
reader,
parseRes(buffer.slice(0, index) + '', rawHeaderNames)
);
var headerLen = index + crlf.length;
size -= headerLen;
buffer = buffer.slice(headerLen);
}
}
buffer && reader.push(buffer);
stream.on('error', function (err) {
reader.emit('error', err);
});
stream.pipe(reader);
}
callback(reader, null, size);
};
reader.on('data', function (data) {
buffer = buffer ? Buffer.concat([buffer, data]) : data;
if (HEADERS_SEP_RE.test(buffer + '')) {
response(null, RegExp.$1);
} else if (buffer.length > MAX_HEADERS_SIZE) {
response();
}
});
reader.on('error', response);
reader.on('end', response);
} else {
callback(reader, range, size);
}
}
function addRangeHeaders(res, range, size) {
if (!range) {
return;
}
var headers = res.headers;
headers['content-range'] =
'bytes ' + (range.start + '-' + range.end) + '/' + size;
headers['accept-ranges'] = 'bytes';
headers['content-length'] = range.end - range.start + 1;
res.statusCode = 206;
}
function isAutoCors(req, rule) {
if (rule.lineProps.disableAutoCors || rule.lineProps.disabledAutoCors) {
return false;
}
return !req.disable.autoCors && req.headers.origin;
}
function sendResponse(rule, res, reader, req) {
if (isAutoCors(req, rule)) {
util.setResCors(reader, ENABLE_OPTIONS, req);
}
rule.isLoc = 1;
res.response(reader);
}
function handleLocHref(req, rule, handleRes) {
var body = util.getMatcherValue(rule) || '';
var isJs = JS_RE.test(body);
var isHtml = HTML_RE.test(body);
var isReplace = REPLACE_RE.test(body);
if (isJs) {
body = body.substring(3);
} else if (isHtml) {
body = body.substring(5);
} else if (isReplace) {
body = body.substring(8);
} else {
isJs = req.headers['sec-fetch-dest'] === 'script';
}
var type = isJs ? 'application/javascript' : 'text/html';
if (body) {
body = urlToStr(body);
if (util.compareUrl(body.replace(HASH_RE, ''), req.fullUrl)) {
return false;
}
if (isReplace) {
body = 'window.location.replace("' + body + '");';
} else {
body = 'window.location.href = "' + body + '";';
}
if (!isJs) {
body = '<script>' + body + '</script>';
}
}
handleRes({
statusCode: 200,
body: body,
headers: {
'content-type': type + '; charset=utf-8'
}
});
return true;
}
module.exports = function (req, res, next) {
var options = req.options;
var config = this.config;
var protocol = options && options.protocol;
if (!protoMgr.isFileProxy(protocol)) {
return next();
}
var rules = req.rules;
var rule = rules.rule;
if (protoMgr.isLocHref(protocol)) {
if (handleLocHref(req, rule, render)) {
return;
}
req.isWebProtocol = true;
req.options = util.parseUrl(req.fullUrl);
return next();
}
if (isAutoCors(req, rule) && req.method === 'OPTIONS') {
var optsRes = util.wrapResponse({ statusCode: 200 }, rule.matcher);
return sendResponse(rule, res, optsRes, req);
}
var isTpl = TPL_RE.test(protocol);
var defaultType = mime.lookup(
util.getPureUrl(req.fullUrl),
'text/html'
);
delete rules.proxy;
delete rules.host;
if (rule.value) {
var body = util.removeProtocol(rule.value, true);
var isRawFile = isRawFileProtocol(protocol);
var reader = isRawFile
? getRawResByValue(body)
: {
statusCode: 200,
body: body,
headers: {
'content-type':
(rule.key ? mime.lookup(rule.key, defaultType) : defaultType) +
'; charset=utf-8'
}
};
if (isTpl) {
reader.realUrl = rule.matcher;
render(reader);
} else {
if (!isRawFile) {
var size = Buffer.byteLength(body);
var range = util.parseRange(req, size);
if (range) {
body = Buffer.from(body);
reader.body = body.slice(range.start, range.end + 1);
addRangeHeaders(reader, range, size);
} else {
addLength(reader, size);
}
}
reader = util.wrapResponse(reader, rule.matcher);
sendResponse(rule, res, reader, req);
}
return;
}
readFiles(util.getRuleFiles(rule, req), function (err, path, size, buffer, svrRes) {
if (err) {
if (CROSS_RE.test(protocol)) {
var fullUrl = RegExp.$1 ? req.fullUrl.replace(HTTP_RE, 'https:') : req.fullUrl;
extend(options, util.parseUrl(fullUrl));
return next();
}
var is502 = err.code > 0 && err.code != 404;
var notFound = util.wrapResponse({
statusCode: is502 ? 502 : 404,
body: is502 ? util.encodeHtml(err.message) : 'Not found ' + (rule.key ? 'key' : 'file') +
' <strong>' + util.encodeHtml(rule.key || path) + '</strong>',
headers: { 'content-type': 'text/html; charset=utf-8' }
}, rule.matcher);
return sendResponse(rule, res, notFound, req);
}
var type = buffer != null && svrRes && svrRes.headers && svrRes.headers['x-whistle-response-type'];
type = type || mime.lookup(rule._suffix || path, defaultType);
var headers = {
server: config.appName,
'content-type': type + (util.isText(type) ? '; charset=utf-8' : '')
};
if (isTpl) {
var reader = {
statusCode: 200,
realUrl: path,
headers: headers
};
if (buffer != null) {
reader.body = buffer + '';
render(reader);
} else {
fs.readFile(path, UTF8_OPTIONS, function (err, data) {
if (err) {
return util.emitError(req, err);
}
reader.body = data;
render(reader);
});
}
} else {
req.localFileSize = size;
getRawResByPath(
protocol,
path,
req,
size,
function (reader, range, realSize) {
reader.realUrl = path;
reader.statusCode = reader.statusCode || 200;
reader.headers = reader.headers || headers;
addRangeHeaders(reader, range, size);
!range && addLength(reader, realSize);
sendResponse(rule, res, reader, req);
},
buffer
);
}
});
function render(reader) {
if (reader.body) {
if (VAR_RE.test(reader.body)) {
var data = util.getQueryString(req.fullUrl);
data = data && qs.parse(data);
if (data) {
reader.body = reader.body.replace(QUERY_VAR_RE, function (all, matched1, matched2) {
if (all[0] === '$') {
return all;
}
var value = data[matched1 || matched2];
return value === undefined ? all : util.getQueryValue(value);
}
);
}
reader.body = rulesMgr.resolveTplVar(reader.body, req);
}
addLength(reader, Buffer.byteLength(reader.body));
} else {
addLength(reader, 0);
}
reader = util.wrapResponse(reader, reader.realUrl);
sendResponse(rule, res, reader, req);
}
};