UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

1,976 lines (1,742 loc) 102 kB
var http = require('http'); var path = require('path'); var os = require('os'); var fs = require('fs'); var vm = require('vm'); var net = require('net'); var crypto = require('crypto'); var fse = require('fs-extra2'); var qs = require('querystring'); var extend = require('extend'); var LRU = require('lru-cache'); var iconv = require('iconv-lite'); var zlib = require('zlib'); var dns = require('dns'); var mime = require('mime'); var PipeStream = require('pipestream'); var protoMgr = require('../rules/protocols'); var logger = require('./logger'); var config = require('../config'); var isUtf8 = require('./is-utf8'); var fileMgr = require('./file-mgr'); var httpMgr = require('./http-mgr'); var ReplacePatternTransform = require('./replace-pattern-transform'); var common = require('./common'); var proc = require('./process'); var parseUrl = require('./parse-url'); var h2Consts = config.enableH2 ? require('http2').constants : {}; var protocols = protoMgr.protocols; var resProtocols = protoMgr.resProtocols; var uid = config.uid; var parseQuery = common.parseQuery; var supportsBr = common.supportsBr; var isLocalIp = common.isLocalIp; var getStat = common.getStat; var toBuffer = common.toBuffer; var pendingFiles = {}; var localIpCache = new LRU({ max: 120 }); var LOCALHOST = '127.0.0.1'; var aliasProtocols = protoMgr.aliasProtocols; var CONTEXT = vm.createContext(); var END_SLASH_RE = /[/\\]$/; var GEN_URL_RE = /^\s*(?:https?:)?\/\/\w[^\s]*\s*$/i; var CORS_KEY_RE = /^(?:enable|use-credentials|useCredentials|credentials)$/i; var NON_LATIN1_RE = /[^\x00-\xFF]/; var SCRIPT_START = toBuffer('<script'); var SCRIPT_END = toBuffer('</script>'); var STYLE_START = toBuffer('<style>'); var STYLE_END = toBuffer('</style>'); var PROXY_RE = /^x?(?:socks|https?-proxy|proxy|internal(?:-https)?-proxy)$/; var SEP_RE = /[|&]/; var ctxTimer; var END_RE = /[/\\]$/; var EXPIRED_SEC = -123456; var EXP_COOKIE = { maxAge: EXPIRED_SEC, path: '/' }; var EXP_SECURE_COOKIE = { maxAge: EXPIRED_SEC, path: '/', secure: true }; var EXP_COOKIE_D = { maxAge: EXPIRED_SEC, path: '/' }; var EXP_SECURE_COOKIE_D = { maxAge: EXPIRED_SEC, path: '/', secure: true }; var resetContext = function () { ctxTimer = null; CONTEXT = vm.createContext(); }; var TIMEOUT_ERR = common.TIMEOUT_ERR; var SUB_MATCH_RE = /\$[&\d]/; var PROTO_NAME_RE = /^([\w.-]+):\/\//; var replacePattern = ReplacePatternTransform.replacePattern; var cryptoConsts = crypto.constants || {}; var EMPTY_BUFFER = toBuffer(''); var encodeHtml = common.encodeHtml; var lowerCaseify = common.lowerCaseify; var removeIPV6Prefix = common.removeIPV6Prefix; var hasBody = common.hasBody; var hasProtocol = common.hasProtocol; var removeProtocol = common.removeProtocol; var getTlsOptions = common.getTlsOptions; var isUrl = common.isUrl; var joinIpPort = common.joinIpPort; var getContentEncoding = common.getContentEncoding; var getUnzipStream = common.getUnzipStream; var toLowerCase = common.toLowerCase; var safeEncodeURIComponent = common.safeEncodeURIComponent; var encodeNonLatin1Char = common.encodeNonLatin1Char; var toUpperCase = common.toUpperCase; var getCharset = common.getCharset; var hasRequestBody = common.hasRequestBody; var parseRawJson = common.parseRawJson; var getMatcher = common.getMatcher; var getMatcherValue = common.getMatcherValue; var getRemoteAddr = common.getRemoteAddr; var getRemotePort = common.getRemotePort; var workerIndex = process.env && process.env.workerIndex; var INTERNAL_ID = Date.now().toString(16) + Math.floor(Math.random() * 10000000).toString(16); var pluginMgr; var RESOLVE_KEY_RE = /^re[qs]Merge:\/\//; var CONTROL_RE = /[\u001e\u001f\u200e\u200f\u200d\u200c\u202a\u202d\u202e\u202c\u206e\u206f\u206b\u206a\u206d\u206c]+/g; var MULTI_LINE_VALUE_RE = /^[^\n\r\S]*(```+)[^\n\r\S]*(\S+)[^\n\r\S]*[\r\n](?:([\s\S]*?)[\r\n])??[^\n\r\S]*\1\s*$/gm; var SPACE_RE = /\s/; var PATH_RE = /^[\w./-]+$/; var SLASH_RE = /[\\/]/; var ROOT_STYLE = ':root{background-color:#121212;color:#f0f0f0;}'; var THEME_STYLE = '<style>@media (prefers-color-scheme: dark) {:not([data-theme="light"])' + ROOT_STYLE + '}\n' + '[data-theme="dark"]' + ROOT_STYLE + '</style>\n'; var SNI_PLUGIN_HEADER = 'x-whistle-sni-plugin-' + uid; var INTERNAL_ID_HEADER = 'x-whistle-internal-id'; var TEMP_TUNNEL_DATA_HEADER = 'x-whistle-tunnel-data-' + uid; var TUNNEL_DATA_HEADER = 'x-whistle-tunnel-data'; var FWD_HOST_HEADER = 'x-forwarded-host'; var FWD_PROPS_HEADER = 'x-whistle-forwarded-props'; var REAL_HOST_HEADER = common.REAL_HOST_HEADER; var HTTPS_PROTO_HEADER = 'x-forwarded-proto'; var clientIpKey = common.CLIENT_IP_HEADER; var clientInfoKey = config.CLIENT_INFO_HEADER; exports.SNI_PLUGIN_HEADER = SNI_PLUGIN_HEADER; exports.INTERNAL_ID_HEADER = INTERNAL_ID_HEADER; exports.TEMP_TUNNEL_DATA_HEADER = TEMP_TUNNEL_DATA_HEADER; exports.TUNNEL_DATA_HEADER = TUNNEL_DATA_HEADER; exports.REAL_HOST_HEADER = REAL_HOST_HEADER; exports.HTTPS_PROTO_HEADER = HTTPS_PROTO_HEADER; exports.ADDITIONAL_HEAD = 'x-whistle-additional-headers'; workerIndex = workerIndex >= 0 ? common.padLeft(config.workerIndex, 3) : ''; exports.sendRes = common.sendRes; exports.THEME_STYLE = THEME_STYLE; exports.encodeNonLatin1Char = encodeNonLatin1Char; exports.isJson = common.isJson; exports.encodeURIComponent = safeEncodeURIComponent; exports.getStat = getStat; exports.toLowerCase = toLowerCase; exports.toUpperCase = toUpperCase; exports.getCharset = getCharset; exports.getContentEncoding = getContentEncoding; exports.getUnzipStream = getUnzipStream; exports.hasRequestBody = hasRequestBody; exports.TIMEOUT_ERR = TIMEOUT_ERR; exports.getTlsOptions = getTlsOptions; exports.encodeHtml = encodeHtml; exports.hasProtocol = hasProtocol; exports.removeProtocol = removeProtocol; exports.parseRawJson = parseRawJson; exports.getMatcherValue = getMatcherValue; exports.setProtocol = common.setProtocol; exports.getProtocol = common.getProtocol; exports.replaceProtocol = common.replaceProtocol; exports.getMethod = common.getMethod; exports.sendGzip = common.sendGzip; exports.sendGzipText = common.sendGzipText; exports.getPureUrl = common.getPureUrl; exports.isTunnelHost = common.isTunnelHost; exports.joinIpPort = joinIpPort; exports.isWebSocket = common.isWebSocket; exports.wrapRuleValue = common.wrapRuleValue; exports.createTransform = common.createTransform; exports.readFileSync = common.readFileTextSync; exports.parseHeaders = common.parseHeaders; exports.connect = common.connect; exports.isUrl = isUrl; exports.workerIndex = workerIndex; exports.proc = proc; exports.INTERNAL_ID = INTERNAL_ID; // 避免属性被 stringify ,减少冗余数据传给前端 exports.PLUGIN_VALUES = Symbol('values'); exports.PLUGIN_MENU_CONFIG = Symbol('menuConfig'); exports.PLUGIN_INSPECTOR_CONFIG = Symbol('inspectorConfig'); exports.drain = require('./drain'); exports.isWin = process.platform === 'win32'; exports.isUtf8 = isUtf8; exports.WhistleTransform = require('./whistle-transform'); exports.ReplacePatternTransform = ReplacePatternTransform; exports.replacePattern = replacePattern; exports.ReplaceStringTransform = require('./replace-string-transform'); exports.SpeedTransform = require('./speed-transform'); exports.FileWriterTransform = require('./file-writer-transform'); exports.getServer = require('hagent').getServer; exports.parseUrl = parseUrl; exports.parseQuery = parseQuery; exports.localIpCache = localIpCache; exports.getRemoteAddr = getRemoteAddr; exports.getRemotePort = getRemotePort; exports.listenerCount = require('./patch').listenerCount; exports.EMPTY_BUFFER = EMPTY_BUFFER; var NOT_SUPPORTED_ERR = new Error('Unsupported'); NOT_SUPPORTED_ERR.code = 502; function request(options, callback) { if (options && options.isInternalReq) { return callback && callback(NOT_SUPPORTED_ERR, '', ''); } return httpMgr.request(common.setInternalOptions(options, config, true), callback); } exports.request = request; function getInlineKey(key, file) { return file ? key + '\n\r' + file : key; } exports.getInlineKey = getInlineKey; function resolveInlineValues(str, inlineValues, file) { str = str && str.replace(CONTROL_RE, '').trim(); if (!str || str.indexOf('```') === -1) { return str; } return str.replace(MULTI_LINE_VALUE_RE, function (_, __, key, value) { key = getInlineKey(key, file); if (inlineValues && inlineValues[key] == null) { inlineValues[key] = value || ''; } return ''; }); } exports.resolveInlineValues = resolveInlineValues; exports.getPluginFile = function(name) { return 'Plugin: ' + name; }; exports.toPrivateValues = function(vals, file) { if (!vals) { return vals; } var keys = Object.keys(vals); if (!keys.length) { return vals; } var result = {}; keys.forEach(function(key) { result[getInlineKey(key, file)] = vals[key]; }); return result; }; function setSecureOptions(options) { var secureOptions = cryptoConsts.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION; if (secureOptions) { options.secureOptions = secureOptions; } return options; } exports.setSecureOptions = setSecureOptions; function noop(_) { return _; } exports.noop = noop; function isCiphersError(e) { var c = e.code; return ( c === 'EPROTO' || c === 'ERR_SSL_BAD_ECPOINT' || c === 'ERR_SSL_VERSION_OR_CIPHER_MISMATCH' || String(e.message).indexOf('disconnected before secure TLS connection was established') !== -1 ); } exports.isCiphersError = isCiphersError; exports.setFramesMode = function(headers, enable) { if (enable) { headers['x-whistle-frames-mode'] = '1'; } else { delete headers['x-whistle-frames-mode']; } }; function getScriptProps(props) { var result = ''; if (props['use-credentials'] || props.useCredentials){ result += ' crossorigin="use-credentials"'; } else if (props.anonymous) { result += ' crossorigin="anonymous"'; } else if (props.crossorigin) { result += ' crossorigin'; } if (props.defer) { result += ' defer'; } if (props.async) { result += ' async'; } if (props.nomodule) { result += ' nomodule'; } if (props.module) { result += ' type="module"'; } else if (props.importmap) { result += ' type="importmap"'; } else if (props.speculationrules) { result += ' type="speculationrules"'; } return result; } function wrapJs(js, charset, isUrl, props) { if (!js) { return ''; } if (isUrl) { return toBuffer('<script' + getScriptProps(props) + ' src="' + js + '"></script>', charset); } return Buffer.concat([SCRIPT_START, toBuffer(getScriptProps(props) + '>'), toBuffer(js, charset), SCRIPT_END]); } function wrapCss(css, charset, isUrl) { if (!css) { return ''; } if (isUrl) { return toBuffer('<link rel="stylesheet" href="' + css + '" />', charset); } return Buffer.concat([STYLE_START, toBuffer(css, charset), STYLE_END]); } var MAX_LEN = 1024 * 1024 * 5; exports.getLatestVersion = function(registry, cb, field) { if (registry && typeof registry !== 'string') { var name = registry.moduleName; registry = registry.registry || (registry.installRegistry && registry.installRegistry[0]) || 'https://registry.npmjs.org'; registry = registry.replace(/\/$/, '') + '/' + name; } if (!registry) { return cb(); } request( { url: registry, maxLength: MAX_LEN, responseType: 'json' }, function (_, body) { if (isString(field)) { body = body && body[field]; } else { body = body && body['dist-tags']; body = body && body['latest']; } cb(body); } ); }; exports.getRegistry = function(pkg) { var registry = pkg.whistleConfig && pkg.whistleConfig.registry; return common.getRegistry(registry); }; exports.isEmptyObject = common.isEmptyObject; exports.isGroup = common.isGroup; exports.addTrailerNames = common.addTrailerNames; exports.removeIllegalTrailers = common.removeIllegalTrailers; exports.isHead = common.isHead; exports.hasBody = hasBody; var ESTABLISHED_CTN = 'HTTP/1.1 200 Connection Established\r\nProxy-Agent: ' + config.appName + '\r\n\r\n'; exports.setEstablished = function (socket) { socket.write(ESTABLISHED_CTN); }; var PORT_RE = /:\d*$/; exports.changePort = function(url, port) { var protocol = ''; var index = url.indexOf('://'); if (index !== -1) { index += 3; protocol = url.substring(0, index); url = url.substring(index); } index = url.indexOf('/'); if (index != -1) { var host = url.substring(0, index); if (net.isIPv6(host)) { host = '[' + host + ']'; } else { host = host.replace(PORT_RE, ''); } url = host + ':' + port + url.substring(index); } return protocol + url; }; function handleStatusCode(statusCode, headers) { if (statusCode == 401) { headers['www-authenticate'] = 'Basic realm=User Login'; } else if (statusCode == 407) { headers['proxy-authenticate'] = 'Basic realm=User Login'; } return headers; } exports.handleStatusCode = handleStatusCode; function getStatusCode(statusCode) { statusCode |= 0; return statusCode < 100 || statusCode > 999 ? 0 : statusCode; } exports.getStatusCode = getStatusCode; function compare(v1, v2) { return v1 == v2 ? 0 : v1 > v2 ? -1 : 1; } exports.compare = compare; var scriptCache = {}; var VM_OPTIONS = { displayErrors: false, timeout: 60 }; var MAX_SCRIPT_SIZE = 1024 * 256; var MAX_SCRIPT_CACHE_COUNT = 64; var MIN_SCRIPT_CACHE_COUNT = 32; function getScript(content) { content = content.trim(); var len = content.length; if (!len || len > MAX_SCRIPT_SIZE) { return; } var script = scriptCache[content]; delete scriptCache[content]; var list = Object.keys(scriptCache); if (list.length > MAX_SCRIPT_CACHE_COUNT) { list = list .map(function (content) { var script = scriptCache[content]; script.content = content; return script; }) .sort(function (a, b) { return compare(a.time, b.time); }) .splice(0, MIN_SCRIPT_CACHE_COUNT); scriptCache = {}; list.forEach(function (script) { scriptCache[script.content] = { script: script.script, time: script.time }; }); } script = scriptCache[content] = script || { script: new vm.Script('(function(){\n' + content + '\n})()') }; script.time = Date.now(); return script.script; } function clearContext() { Object.keys(CONTEXT).forEach(function (key) { delete CONTEXT[key]; }); if (!ctxTimer) { ctxTimer = setTimeout(resetContext, 30000); } } exports.execScriptSync = function(script, context) { try { if ((script = getScript(script))) { CONTEXT.console = {}; ['fatal', 'error', 'warn', 'info', 'log', 'debug'].forEach(function ( level ) { CONTEXT.console[level] = logger[level]; }); Object.keys(context).forEach(function (key) { CONTEXT[key] = context[key]; }); script.runInContext(CONTEXT, VM_OPTIONS); } return true; } catch (e) { logger.error(e); } finally { clearContext(); } }; function checkWriterFile(file, callback, force) { if (force) { return callback(true); } getStat(file, function (err) { if (!err || err.code === 'ENOTDIR') { return callback(); } if (err.code === 'ENOENT') { return callback(true); } callback(err); }); } function getFileWriter(file, callback, force) { if (!file) { return callback(); } if (END_RE.test(file)) { file = path.join(file, 'index.html'); } if (!force && pendingFiles[file]) { return callback(); } var execCb = function (writer) { delete pendingFiles[file]; callback(writer); }; pendingFiles[file] = 1; checkWriterFile(file, function (notExists) { if (!notExists) { return execCb(); } fse.ensureFile(file, function (err) { if (err) { logger.error(err); return execCb(); } execCb(fs.createWriteStream(file).on('error', logger.error)); }); }, force ); } function handleCallback(list, fn, callback) { if (!Array.isArray(list)) { list = [list]; } var len = list.length; if (!len) { return callback(); } var result = []; list.forEach(function(item, i) { fn(item, function(value) { result[i] = value; if (--len === 0) { callback.apply(null, result); } }); }); } exports.getFileWriters = function (files, callback, force) { handleCallback(files, function(file, cb) { getFileWriter(file, cb, force); }, callback); }; exports.toBuffer = toBuffer; function getErrorStack(err) { if (!err) { return ''; } var stack; try { stack = err.stack; } catch (e) {} stack = stack || err.message || err; var result = [ 'From: ' + config.appName + '@' + config.version, 'Node: ' + process.version, 'Host: ' + hostname, 'Date: ' + formatDate(), stack ]; return result.join('\r\n'); } exports.getErrorStack = getErrorStack; function formatDate(now) { now = now || new Date(); return now.toLocaleString(); } exports.formatDate = formatDate; var REG_EXP_RE = /^\/(.+)\/(i?u?|ui)$/; function isRegExp(regExp) { return REG_EXP_RE.test(regExp); } exports.isRegExp = isRegExp; var ORIG_REG_EXP = /^\/(.+)\/([igmu]{0,4})$/; var FLAGS_RE = /[igmu]{2}/; function isOriginalRegExp(regExp) { if (!ORIG_REG_EXP.test(regExp) || FLAGS_RE.test(regExp.$2)) { return false; } return true; } exports.isOriginalRegExp = isOriginalRegExp; function toOriginalRegExp(regExp) { regExp = ORIG_REG_EXP.test(regExp); try { regExp = regExp && new RegExp(RegExp.$1, RegExp.$2); } catch (e) { regExp = null; } return regExp; } exports.toOriginalRegExp = toOriginalRegExp; exports.emitError = function (obj, err) { if (obj) { obj.once('error', noop); obj.emit('error', err || new Error('Unknown')); } }; exports.startWithList = function (buf, subBuf, start) { var len = subBuf.length; if (!len) { return false; } start = start || 0; for (var i = 0; i < len; i++) { if (buf[i + start] != subBuf[i]) { return false; } } return true; }; exports.endWithList = function (buf, subBuf, end) { var subLen = subBuf.length; if (!subLen) { return false; } if (!(end >= 0)) { end = buf.length - 1; } for (var i = 0; i < subLen; i++) { if (subBuf[subLen - i - 1] != buf[end - i]) { return false; } } return true; }; function isEnable(req, name) { return req.enable[name] && !req.disable[name]; } exports.isEnable = isEnable; exports.isDisable = function(req, name) { return req.disable[name] && !req.enable[name]; }; exports.formatUrl = common.formatUrl; exports.isKeepClientId = function(req, proxyUrl) { if (isEnable(req, 'keepClientId')) { return true; } var disable = req.disable; if (disable.clientId || disable.clientID) { return false; } var enable = req.enable; return enable.clientId || enable.clientID || proxyUrl; }; exports.getInternalHost = function (req, host) { if (isEnable(req, 'useLocalHost')) { return 'local.wproxy.org'; } if (host && isEnable(req, 'useSafePort')) { var index = host.indexOf(':'); if (index !== -1) { host = host.substring(0, index); } host += ':8899'; } return host; }; function isAuthCapture(req) { var e = req.enable || ''; var d = req.disable || ''; return ( (e.authCapture || e.authIntercept) && !d.authCapture && !d.authIntercept ); } exports.isAuthCapture = isAuthCapture; exports.toRegExp = function toRegExp(regExp, ignoreCase) { regExp = REG_EXP_RE.test(regExp); try { regExp = regExp && new RegExp(RegExp.$1, ignoreCase ? 'i' : RegExp.$2); } catch (e) { regExp = null; } return regExp; }; var isString = common.isString; var getFullUrl = common.getFullUrl; exports.isString = isString; exports.getFullUrl = getFullUrl; function disableCSP(headers) { delete headers['content-security-policy']; delete headers['content-security-policy-report-only']; delete headers['x-content-security-policy']; delete headers['x-content-security-policy-report-only']; delete headers['x-webkit-csp']; } exports.disableCSP = disableCSP; var interfaces = os.networkInterfaces(); var hostname = os.hostname(); var simpleHostname = ''; var cpus = os.cpus(); var addressList = []; var usiTimer; function updateSystyemInfo() { clearTimeout(usiTimer); interfaces = os.networkInterfaces(); hostname = os.hostname(); addressList = []; common.walkInterfaces(function (info) { addressList.push(info.address.toLowerCase()); }); usiTimer = setTimeout(updateSystyemInfo, 36000); } updateSystyemInfo(); process.on('w2NetworkInterfacesChange', updateSystyemInfo); if (isString(hostname)) { simpleHostname = hostname.replace(/[^\w.-]+/g, '').substring(0, 20); simpleHostname = simpleHostname ? simpleHostname + '.' : ''; } function createHash(str) { return crypto.createHash('sha256').update(str).digest('hex'); } exports.createHash = createHash; var clientId = [ hostname, os.platform(), os.release(), os.arch(), cpus.length, cpus[0] && cpus[0].model, config.clientId ]; clientId = config.clientId = simpleHostname + crypto .createHmac('sha256', config.CLIENT_ID_HEADER) .update(clientId.join('\r\n')) .digest('base64'); config.runtimeId = simpleHostname + crypto .createHmac('sha256', config.CLIENT_ID_HEADER) .update(clientId + '\r\n' + Math.random() + '\r\n' + Date.now()) .digest('base64') + '/' + config.port; config.runtimeHeaders = { 'x-whistle-runtime-id': config.runtimeId }; config.pluginHeaders = { 'x-whistle-runtime-id': config.runtimeId }; config.pluginHeaders[INTERNAL_ID_HEADER] = INTERNAL_ID; config.pluginHeaders[config.PLUGIN_HOOK_NAME_HEADER] = config.PLUGIN_HOOKS.UI; exports.setClientId = function ( headers, enable, disable, clientIp, isInternalProxy ) { if (disable && (disable.clientId || disable.clientID)) { return; } enable = enable || ''; if ( enable.clientId || enable.clientID || isInternalProxy ) { var id = getClientId(headers); if ( (enable.multiClient || isInternalProxy) && !enable.singleClient && !disable.multiClient ) { if (headers[config.CLIENT_ID_HEADER]) { return; } if (!isLocalAddress(clientIp)) { id += '/' + clientIp; } } headers[config.CLIENT_ID_HEADER] = id; } }; function getClientId(headers) { var id = headers[config.CLIENT_ID_HEADER]; var idKey = config.cidKey; if (!idKey || (id && !config.overCidKey)) { return id || clientId; } return headers[idKey] || id || clientId; } exports.getClientId = getClientId; exports.getUpdateUrl = common.getUpdateUrl; exports.getTunnelKey = function (conf) { var tunnelKey = conf.tunnelKey || conf.tunnelKeys; if (isString(tunnelKey)) { tunnelKey = tunnelKey.toLowerCase().split(/[:,|]/); tunnelKey = tunnelKey.map(trim).filter(noop); return tunnelKey.slice(0, 10); } }; function getComposerClientId(headers) { var clientId = headers[config.COMPOSER_CLIENT_ID_HEADER]; if (clientId) { delete headers[config.COMPOSER_CLIENT_ID_HEADER]; return clientId; } } exports.getComposerClientId = getComposerClientId; exports.removeClientId = function (headers) { delete headers[config.CLIENT_ID_HEADER]; }; function networkInterfaces() { return interfaces; } function getHostname() { return hostname; } exports.networkInterfaces = networkInterfaces; exports.hostname = getHostname; function getProxyTunnelPath(req, isHttps) { var host = req._phost && req._proxyTunnel && req.headers.host; if (isString(host)) { return host.indexOf(':') !== -1 ? host : host + ':' + (isHttps ? 443 : 80); } } exports.getProxyTunnelPath = getProxyTunnelPath; function isLocalAddress(address) { if (isLocalIp(address)) { return true; } address = address.toLowerCase(); if (address[0] === '[') { address = address.slice(1, -1); } if (address == '0:0:0:0:0:0:0:1') { return true; } return localIpCache.get(address) || addressList.indexOf(address) !== -1; } exports.isLocalAddress = isLocalAddress; function isLocalHost(host) { return host === 'localhost' || isLocalAddress(host); } exports.isLocalHost = isLocalHost; function parseHost(host) { var index; if (host[0] === '[') { index = host.indexOf(']'); return [host.substring(1, index), host.substring(index + 2)]; } index = host.indexOf(':'); if (index === -1 || host.indexOf(':', index + 1) !== -1) { return [host, '']; } return [host.substring(0, index), host.substring(index + 1)]; } exports.parseHost = parseHost; function compareUrl(url, fullUrl) { url = common.getAbsUrl(url, fullUrl); if (url === fullUrl) { return true; } try { return url === decodeURIComponent(fullUrl); } catch (e) {} } exports.compareUrl = compareUrl; function getPath(url, noProtocol) { if (url) { url = common.getPureUrl(url); var index = noProtocol ? -1 : url.indexOf('://'); url = index > -1 ? url.substring(index + 3) : url; } return url; } exports.getPath = getPath; function getFilename(url) { if (typeof url == 'string' && (url = getPath(url).trim())) { var index = url.lastIndexOf('/'); if (index != -1) { url = url.substring(index + 1); } else { url = null; } } else { url = null; } return url || 'index.html'; } exports.getFilename = getFilename; function disableReqCache(headers) { delete headers['if-modified-since']; delete headers['if-none-match']; delete headers['last-modified']; delete headers.etag; headers['pragma'] = 'no-cache'; headers['cache-control'] = 'no-cache'; } exports.disableReqCache = disableReqCache; function disableResStore(headers) { headers['cache-control'] = 'no-store'; headers['expires'] = new Date(Date.now() - 60000000).toGMTString(); headers['pragma'] = 'no-cache'; delete headers.tag; } exports.disableResStore = disableResStore; function notNull(value) { return value != null; } var HTTP_PROTO_RE = /^(?:ws|http)s?:/; function parsePathReplace(urlPath, params, delPaths) { if ((!params && !delPaths) || !HTTP_PROTO_RE.test(urlPath)) { return; } var index = urlPath.indexOf('://'); if (index == -1) { return; } index = urlPath.indexOf('/', index + 3) + 1; if (!index) { return; } var curPath = urlPath.substring(index); params && Object.keys(params).forEach(function (pattern) { var value = params[pattern]; value = value == null ? '' : value + ''; if (isOriginalRegExp(pattern) && (pattern = toOriginalRegExp(pattern))) { curPath = curPath.replace(pattern, value); } else if (pattern) { curPath = curPath.split(pattern).join(value); } }); if (curPath && delPaths) { var query = ''; var qIdx = curPath.indexOf('?'); if (qIdx !== -1) { query = curPath.substring(qIdx); curPath = curPath.substring(0, qIdx); } if (curPath) { if (delPaths.all) { delete delPaths.all; curPath = query; } else { curPath = curPath.split('/'); var len = curPath.length; var last = delPaths.last; if (last) { delete delPaths.last; delPaths[len - 1] = 1; } Object.keys(delPaths).forEach(function (key) { key = +key; key = key < 0 ? len + key : key; if (key >= 0 && key < len) { curPath[key] = null; } }); curPath = curPath.filter(notNull); if (last && curPath[curPath.length - 1]) { curPath.push(''); } curPath = curPath.join('/'); } } curPath += query; } var newUrl = urlPath.substring(0, index) + curPath; return newUrl === urlPath ? null : newUrl; } exports.parsePathReplace = parsePathReplace; function wrapResponse(res, realUrl) { var newRes = common.createTransform(); newRes.statusCode = res.statusCode; newRes.rawHeaderNames = res.rawHeaderNames; newRes.headers = lowerCaseify(res.headers); newRes.headers['x-server'] = config.appName; res.body != null && newRes.push(Buffer.isBuffer(res.body) ? res.body : String(res.body)); newRes.push(null); newRes.isCustomRes = true; newRes.realUrl = realUrl; return newRes; } exports.wrapResponse = wrapResponse; function wrapGatewayError(body) { return wrapResponse({ statusCode: 502, headers: { 'content-type': 'text/html; charset=utf8' }, body: body ? '<pre>\n' + encodeHtml(body) + '\n\n\n<a href="javascript:;" onclick="location.reload()"' + '>Reload this page</a>\n</pre>' : '' }); } exports.wrapGatewayError = wrapGatewayError; function sendStatusCodeError(cltRes, svrRes) { delete svrRes.headers['content-length']; cltRes.writeHead(502, svrRes.headers); cltRes.src(wrapGatewayError('Invalid status code: ' + svrRes.statusCode)); } exports.sendStatusCodeError = sendStatusCodeError; exports.getQueryValue = function (value) { if (value && typeof value === 'object') { try { return JSON.stringify(value) || ''; } catch (e) {} } return value || ''; }; function parseInlineJSON(text, isValue) { if (!isValue || SPACE_RE.test(text)) { return; } return parseQuery(text, null, null, true); } function _parseJSON(data, resolveKeys) { if (typeof data === 'object') { return data; } if (!isString(data) || !(data = data.trim())) { return null; } return parsePureJSON(data, true) || common.parsePlainText(data, resolveKeys === true); } function parseJSON(data) { return _parseJSON(data); } function parsePureJSON(data, isValue) { return parseRawJson(data) || parseInlineJSON(data, isValue); } exports.parseJSON = parseJSON; function trim(text) { return text && text.trim(); } exports.trim = trim; exports.lowerCaseify = lowerCaseify; var QUERY_PARAM_RE = /^[^\\/]+=/; function tryParseMatcher(text, rule) { var matcher = !text && removeProtocol(getMatcher(rule), true); if (!matcher || matcher.indexOf('=') === -1) { return; } return parseQuery(matcher, null, null, true); } exports.parseRuleJson = function(rules, callback, req) { handleCallback(rules, function(rule, cb) { readRuleList(rule, cb, true, null, null, req); }, callback); }; function getTempFilePath(filePath, rule) { var root = rule.root; if (!root && common.TEMP_PATH_RE.test(filePath)) { rule._suffix = RegExp.$2; return path.join(config.TEMP_FILES_PATH, RegExp.$1); } return joinPath(root, decodePath(filePath)); } function readRuleValue(rule, callback, checkUrl, needRawData, req) { if (!rule) { return callback(); } if (rule.value) { return callback(removeProtocol(rule.value, true)); } var filePath = getMatcherValue(rule); if (checkUrl && GEN_URL_RE.test(filePath)) { return callback(filePath); } var opts = pluginMgr.resolveKey(filePath, rule, req); var readFile; if (opts) { readFile = pluginMgr[needRawData ? 'requestBin' : 'requestText']; return readFile(opts, callback); } filePath = getTempFilePath(filePath, rule); if (!filePath) { return callback(); } readFile = fileMgr[needRawData ? 'readFile' : 'readFileText']; readFile(filePath, callback); } function isGenUrl(value) { return typeof value === 'string' && GEN_URL_RE.test(value); } var CORS_RE = /^re[qs]Cors:\/\//; function isDeep(result) { for (var i = 0, len = result.length; i < len; i++) { if (result[i] === true) { return true; } } } function indexOfQuote(matcher) { return matcher[0] === '"' ? matcher.indexOf('"=') : -1; } function getMatcherJson(rule, value) { var matcher = rule.rawMatcher; var eq = matcher.indexOf('='); if (eq === -1) { return; } var and = matcher.indexOf('&'); if (and !== -1 && and > eq) { return; } matcher = removeProtocol(matcher, true); var len = matcher.length - 1; var first = matcher[0]; var last = matcher[len]; if (first === '{' && last === '}') { return; } var index; if (first === '(' && last === ')') { matcher = matcher.substring(1, len); } else { if (!QUERY_PARAM_RE.test(matcher)) { index = indexOfQuote(matcher); if (index === -1) { return; } } else if (first === '<' && last === '>') { matcher = matcher.substring(1, len); } } if (index == null) { index = indexOfQuote(matcher); } var hasQuote; if (index === -1) { index = matcher.indexOf('='); } else { index += 1; hasQuote = true; } var key = matcher.substring(0, index); value = value || removeProtocol(getMatcher(rule), true); if (value.indexOf(key + '=')) { return; } var result = {}; result[hasQuote ? key.substring(1, index - 1) : key] = value.substring(index + 1); return result; } function readRuleList(rule, callback, isJson, charset, isHtml, req) { if (!rule) { return callback(); } var len = rule.list && rule.list.length; var isBin = protoMgr.isBinProtocol(rule.name); var needRawData = isBin && !isJson; if (!len) { if (isJson) { var val = removeProtocol(getMatcher(rule), true); val = val && val.trim(); var json = getMatcherJson(rule, val) || parsePureJSON(val, QUERY_PARAM_RE.test(val)); if (json) { return callback(json); } } return readRuleValue( rule, isJson ? function (value) { callback(tryParseMatcher(value, rule) || _parseJSON(value, RESOLVE_KEY_RE.test(rule.matcher))); } : callback, false, needRawData, req ); } var result = []; var isJsHtml = isHtml && isBin === 2; var isCssHtml = isHtml && isBin === 3; var isRawList = rule.isRawList; var execCallback = function () { if (--len > 0) { return; } if (isJson) { var deepMerge = isDeep(result); result = result.map(function(text, i) { var item = rule.list[i]; return tryParseMatcher(text, item) || _parseJSON(text, item && RESOLVE_KEY_RE.test(item.matcher)); }).filter(noop); if (result.length > 1) { result.reverse(); if (typeof result[0] !== 'object') { result[0] = {}; } deepMerge && result.unshift(true); callback(extend.apply(null, result)); } else { callback(result[0]); } } else if (isRawList) { callback(result); } else if (isHtml) { result = result.filter(noop); callback(result.length ? result : ''); } else { callback(fileMgr.joinData(result, !isBin, charset)); } }; var isCors = CORS_RE.test(rule.matcher); var checkUrl = isJsHtml || isCssHtml; rule.list.forEach(function (r, i) { if (isJson) { var json = r.jsonObject; var value; if (json) { r.jsonObject = undefined; } else { value = removeProtocol(getMatcher(r), true); json = getMatcherJson(r, value); } if (json) { result[i] = json; return execCallback(); } value = value && value.trim(); if (value) { if (isCors) { if (GEN_URL_RE.test(value)) { json = { origin: value }; } else if (value === '*') { json = { '*': '' }; } else if (CORS_KEY_RE.test(value)) { json = { enable: true }; } } json = json || parsePureJSON(value, QUERY_PARAM_RE.test(value)); if (json) { result[i] = json; return execCallback(); } } } readRuleValue( r, function (value) { if (isHtml && value) { var props = r.lineProps; if (checkUrl) { var isUrl = isGenUrl(value); var wrap = isJsHtml ? wrapJs : wrapCss; value = wrap(isUrl ? value.trim() : value, charset, isUrl, props); } else { value = toBuffer(value, charset); } var strictHtml = props.strictHtml; var safeHtml = props.safeHtml; if (strictHtml || safeHtml) { value._strictHtml = strictHtml; value._safeHtml = safeHtml; r.lineStrict = strictHtml; r.lineSafe = safeHtml; } } result[i] = value; execCallback(); }, checkUrl, needRawData, req ); }); } exports.getRuleValue = function(rules, callback, noBody, charset, isHtml, req) { if (noBody || !rules) { return callback(); } handleCallback(rules, function(rule, cb) { readRuleList(rule, cb, false, charset, isHtml, req); }, callback); }; function decodePath(path) { path = getPath(path, true); try { return decodeURIComponent(path); } catch (e) { logger.error(e); } try { return qs.unescape(path); } catch (e) { logger.error(e); } return path; } exports.getRuleFiles = function(rule, req) { if (rule.key) { return []; } var files = rule.files || [getPath(getUrl(rule))]; var rawFiles = rule.rawFiles || files; var result = []; files.forEach(function (file, i) { var opts = pluginMgr.resolveKey(rawFiles[i], rule, req); if (opts) { result.push(opts); } else if (file = getTempFilePath(file, rule)) { file = fileMgr.convertSlash(file); if (END_SLASH_RE.test(file)) { result.push(file.slice(0, -1)); result.push(file + 'index.html'); } else { result.push(file); } } else { result.push(file); } }); return result; }; exports.getWriteFilePath = function(rule) { var filePath = getPath(getUrl(rule)); return filePath && joinPath(rule.root, decodePath(filePath)); }; function getUrl(rule) { return common.trimUrl(rule && (common.getRuleValue(rule) || rule.url)); } exports.rule = { getMatcher: getMatcher, getUrl: getUrl }; exports.getUrlValue = function(rule) { rule = getUrl(rule); return rule && removeProtocol(rule, true); }; function _getRawType(type) { return typeof type === 'string' ? type.split(';')[0].toLowerCase() : ''; } function getRawType(data) { return _getRawType(data.headers && data.headers['content-type']); } exports.getRawType = getRawType; function getContentType(contentType) { if (contentType && typeof contentType != 'string') { contentType = contentType['content-type'] || contentType.contentType; } contentType = _getRawType(contentType); if (!contentType) { return; } if (contentType.indexOf('javascript') != -1) { return 'JS'; } if (contentType.indexOf('css') != -1) { return 'CSS'; } if (contentType.indexOf('html') != -1) { return 'HTML'; } if (contentType.indexOf('json') != -1) { return 'JSON'; } if (contentType.indexOf('xml') != -1) { return 'XML'; } if (contentType.indexOf('text/') != -1) { return 'TEXT'; } if (contentType.indexOf('image/') != -1) { return 'IMG'; } } exports.getContentType = getContentType; function isText(contentType) { contentType = contentType && getContentType(contentType); return contentType && contentType !== 'IMG'; } exports.isText = isText; function supportHtmlTransform(res, req) { var headers = res.headers; if (getContentType(headers) != 'HTML' || !hasBody(res, req)) { return false; } var contentEncoding = getContentEncoding(headers); //chrome新增了sdch压缩算法,对此类响应无法解码,deflate无法区分deflate还是deflateRaw return !contentEncoding || contentEncoding == 'gzip' || contentEncoding === 'br'; } exports.supportHtmlTransform = supportHtmlTransform; exports.getEnableEncoding = function(enable) { if (!enable) { return; } if (enable.br) { return supportsBr ? 'br' : null; } if (enable.gzip) { return 'gzip'; } if (enable.deflate) { return 'deflate'; } }; function removeUnsupportsHeaders(headers, supportsDeflate) { //只保留支持的zip格式:gzip、deflate if (!headers || !headers['accept-encoding']) { return; } if (config.noGzip) { delete headers['accept-encoding']; return; } var list = headers['accept-encoding'].trim().split(/\s*,\s*/g); var acceptEncoding = []; for (var i = 0, len = list.length; i < len; i++) { var ae = list[i].toLowerCase(); if (ae && ((supportsDeflate && ae == 'deflate') || ae == 'gzip' || (supportsBr && ae === 'br'))) { acceptEncoding.push(ae); } } if ((acceptEncoding = acceptEncoding.join(', '))) { headers['accept-encoding'] = acceptEncoding; } } exports.removeUnsupportsHeaders = removeUnsupportsHeaders; function getZipStream(headers) { switch (getContentEncoding(headers)) { case 'gzip': return zlib.createGzip(); case 'br': return supportsBr && zlib.createBrotliCompress(); case 'deflate': return zlib.createDeflate(); } } exports.isZip = function(encoding, chunk) { if (encoding === 'gzip') { return chunk[0] === 31 && (chunk[1] == null || chunk[1] === 139); } if (encoding === 'br') { return supportsBr; } return encoding === 'deflate'; }; exports.getZipStream = getZipStream; exports.isWhistleTransformData = function (obj) { if (!obj) { return false; } return obj.speed > 0 || obj.delay > 0 || obj.top || obj.body || obj.bottom; }; function getPipeIconvStream(headers) { var pipeStream = new PipeStream(); var charset = getCharset(headers['content-type']); if (charset) { pipeStream.addHead(iconv.decodeStream(charset)); pipeStream.addTail(iconv.encodeStream(charset)); } else { pipeStream.addHead(function (res, next) { var buffer, iconvDecoder; res.on('data', function (chunk) { buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk; resolveCharset(buffer); }); res.on('end', resolveCharset); function resolveCharset(chunk) { if (!charset) { if (chunk && buffer.length < 25600) { return; } charset = !buffer || isUtf8(buffer) ? 'utf8' : 'GB18030'; } if (!iconvDecoder) { iconvDecoder = iconv.decodeStream(charset); next(iconvDecoder); } if (buffer) { iconvDecoder.write(buffer); buffer = null; } !chunk && iconvDecoder.end(); } }); pipeStream.addTail(function (src, next) { next(src.pipe(iconv.encodeStream(charset))); }); } return pipeStream; } exports.getPipeIconvStream = getPipeIconvStream; function getClientIpFH(headers, name) { var val = headers[name]; if (!isString(val)) { return ''; } var index = val.indexOf(','); if (index !== -1) { val = val.substring(0, index); } val = removeIPV6Prefix(val.trim()); return net.isIP(val) && !isLocalAddress(val) ? val : ''; } function getForwardedFor(headers) { var ip = getClientIpFH(headers, clientIpKey); var cipKey = config.cipKey; if (cipKey && (!ip || config.overCipKey)) { ip = getClientIpFH(headers, cipKey) || ip; } return ip; } exports.getForwardedFor = getForwardedFor; function getClientIp(req, ip) { ip = ip || getForwardedFor(req.headers || {}) || getRemoteAddr(req); return isLocalIp(ip) ? LOCALHOST : ip; } exports.getClientIp = getClientIp; function getClientPort(req) { return common.getClientPort(req, config); } exports.getClientPort = getClientPort; exports.removeIPV6Prefix = removeIPV6Prefix; exports.isUrlEncoded = common.isUrlEncoded; function isJSONContent(req) { if (!hasRequestBody(req)) { return false; } return getContentType(req.headers) === 'JSON'; } exports.isJSONContent = isJSONContent; function isProxyPort(proxyPort) { return ( proxyPort == config.port || proxyPort == config.httpsPort || proxyPort == config.httpPort || proxyPort == config.socksPort || proxyPort == config.realPort ); } exports.isProxyPort = isProxyPort; exports.isLocalPHost = function(req, isHttps) { var phost = req._phost; var hostname = phost && phost.hostname; if (!hostname || !isProxyPort(phost.port || (isHttps ? 443 : 80))) { return false; } return isLocalHost(hostname); }; var MULTIPART_RE = /multipart/i; function isMultipart(req) { return MULTIPART_RE.test(req.headers['content-type']); } exports.isMultipart = isMultipart; function getQueryString(url) { var index = url.indexOf('?'); return index == -1 ? '' : url.substring(index + 1); } exports.getQueryString = getQueryString; function replaceQueryString(query, replaceQuery, delProps) { if (replaceQuery && typeof replaceQuery != 'string') { replaceQuery = qs.stringify(replaceQuery); } if (delProps ? (!query && !replaceQuery) : (!query || !replaceQuery)) { return replaceQuery || query; } var queryList = []; var params = {}; var curParams = {}; var name, value; var parseKey = function (item) { var index = item.indexOf('='); if (index == -1) { name = item; value = ''; } else { name = item.substring(0, index); value = item.substring(index + 1); } }; var addValue = function(obj) { var curVal = obj[name]; if (Array.isArray(curVal)) { curVal.push(value); } else if (curVal != null) { curVal = [curVal, value]; } obj[name] = curVal || value; }; if (replaceQuery) { replaceQuery = replaceQuery.split('&').map(function(item) { parseKey(item); if (!delProps || !delProps[name]) { addValue(params); } }); } if (query) { query.split('&').map(function(item) { parseKey(item); if ((!delProps || !delProps[name]) && params[name] == null) { addValue(curParams); } }); } extend(curParams, params); Object.keys(curParams).forEach(function(key) { var val = curParams[key]; if (Array.isArray(val)) { val.forEach(function(v) { queryList.push(key + '=' + v); }); } else { queryList.push(key + '=' + val); } }); return queryList.join('&'); } exports.replaceQueryString = replaceQueryString; exports.replaceUrlQueryString = function(url, queryString) { if (!queryString) { return url; } url = url || ''; var hashIndex = url.indexOf('#'); var hashString = ''; if (hashIndex != -1) { hashString = url.substring(hashIndex); url = url.substring(0, hashIndex); } queryString = replaceQueryString(getQueryString(url), queryString); return ( url.replace(/\?.*$/, '') + (queryString ? '?' + queryString : '') + hashString ); }; exports.decodeBuffer = fileMgr.decode; function setHeaders(data, obj) { if (!data.headers) { data.headers = {}; } Object.keys(obj).forEach(function (key) { data.headers[key] = obj[key]; }); return data; } exports.setHeaders = setHeaders; function setHeader(data, name, value) { if (!data.headers) { data.headers = {}; } data.headers[name] = value; return data; } exports.setHeader = setHeader; function joinPath(root, dir) { if (common.existsUpPath(dir)) { return; } if (!root) { return dir; } var fullPath = path.resolve(root, dir); var slash = END_SLASH_RE.exec(dir || root); return slash && !END_SLASH_RE.test(fullPath) ? fullPath + slash[0] : fullPath; } exports.joinPath = joinPath; function parseRuleProps(list, result) { result = result || {}; if (list) { list.forEach(function(rule) { if (rule = getMatcherValue(rule)) { common.parseProps(rule).forEach(function (action) { result[action] = true; }); } }); } return result; } exports.parseRuleProps = parseRuleProps; exports.parseLineProps = function (str) { str = str && removeProtocol(str, true); if (!str) { return; } var result = {}; str.split(SEP_RE).forEach(function (action) { if (action) { result[action] = true; } }); return result; }; function resolveIgnore(ignore) { var keys = Object.keys(ignore); var exclude = {}; var ignoreAll, disableIgnoreAll; ignore = {}; keys.forEach(function (name) { if (name.indexOf('ignore.') === 0 || name.indexOf('ignore:') === 0) { exclude[name.substring(7)] = 1; return; } if (name.indexOf('-') === 0 || name.indexOf('!') === 0) { name = name.substring(1); if (name === '*') { disableIgnoreAll = true; } else { exclude[name] = 1; } return; } name = name.replace('ignore|', ''); if (name === 'filter' || name === 'ignore') { return; } if ( name === 'allRules' || name === 'allProtocols' || name === 'All' || name === '*' ) { ignoreAll = true; return; } ignore[aliasProtocols[name] || name] = 1; }); if (ignoreAll && !disableIgnoreAll) { protocols.forEach(function (name) { ignore[name] = 1; }); keys = protocols; } else { keys = Object.keys(ignore); } keys.forEach(function (name) { if (exclude[name]) { delete ignore[name]; } }); return { ignoreAll: ignoreAll, exclude: exclude, ignore: ignore }; } function resolveFilter(ignore, filter) { filter = filter || {}; var result = resolveIgnore(ignore); ignore = result.ignore; Object.keys(ignore).forEach(function (name) { if (protocols.indexOf(name) === -1) { filter['ignore|' + name] = true; } else { filter[name] = true; } }); Object.keys(result.exclude).forEach(function (name) { filter['ignore:' + name] = 1; }); if (result.ignoreAll) { filter.allRules = 1; } return filter; } exports.resolveFilter = resolveFilter; exports.isIgnored = function (filter, name) { return ( !filter['ignore:' + name] && (filter[name] || filter['ignore|' + name]) ); }; function exactIgnore(filter, rule) { if (filter['ignore|' + 'pattern=' + rule.rawPattern]) { return true; } if (filter['ignore|' + 'matcher=' + rule.matcher]) {