UNPKG

whistle

Version:

HTTP, HTTP2, HTTPS, Websocket debugging proxy

1,284 lines (1,249 loc) 51.6 kB
var https = require('https'); var http = require('http'); var net = require('net'); var parseUrl = require('../util/parse-url-safe'); var extend = require('extend'); var util = require('../util'); var Transform = require('pipestream').Transform; var h2 = require('../https/h2'); var rules = require('../rules'); var pluginMgr = require('../plugins'); var hparser = require('hparser'); var config = require('../config'); var WhistleTransform = util.WhistleTransform; var SpeedTransform = util.SpeedTransform; var ReplacePatternTransform = util.ReplacePatternTransform; var ReplaceStringTransform = util.ReplaceStringTransform; var FileWriterTransform = util.FileWriterTransform; var formatHeaders = hparser.formatHeaders; var getRawHeaderNames = hparser.getRawHeaderNames; var TIMEOUT = config.timeout < 16000 && config.timeout > 0 ? 0 : 16000; var LOCALHOST = '127.0.0.1'; var CRLF = util.toBuffer('\r\n'); var MAX_RES_SIZE = 1024 * 1024 * (config.strict ? 1 : 2); // 2MB var BIG_MAX_RES_SIZE = 1024 * 1024 * 16; var JSON_RE = /{[\w\W]*}|\[[\w\W]*\]/; var LIKE_JSON_RE = /^\s*[\{\[]/; var NO_STORE_RE = /^no-store$/i; var NO_CACHE_RE = /^(?:no|no-cache|no-store)$/i; var X_RE = /^x/; var clientIpKey = config.CLIENT_IP_HEADER; var BODY_PROTOCOLS = [ 'attachment', 'resReplace', 'resBody', 'resPrepend', 'resAppend', 'htmlBody', 'htmlPrepend', 'htmlAppend', 'jsBody', 'jsPrepend', 'jsAppend', 'cssBody', 'cssPrepend', 'cssAppend', 'resWrite', 'resWriteRaw', 'resMerge' ]; var BODY_PROTOCOLS_LEN = BODY_PROTOCOLS.length; function notAllowCache(resRules) { for (var i = 0; i < BODY_PROTOCOLS_LEN; i++) { if (resRules[BODY_PROTOCOLS[i]]) { return true; } } } function joinArr(src, dest) { if (!src || !dest) { return src || dest; } return src.concat(dest); } function pipeClient(req, client) { if (req._hasError) { client.destroy(); } else if (req.noReqBody) { util.drain(req, function () { if (!req._hasError) { client.end(); } }); } else { req.pipe(client); } } function showDnsError(res, err) { res.response( util.wrapGatewayError('DNS Lookup Failed\r\n' + util.getErrorStack(err)) ); } function setCookies(headers, data) { var newCookies = data.headers['set-cookie']; if (!Array.isArray(newCookies)) { if (!newCookies || typeof newCookies !== 'string') { return; } newCookies = newCookies.split(','); } if (newCookies.length) { var cookies = headers['set-cookie']; var isArray = Array.isArray(cookies); if (!isArray && cookies) { isArray = true; cookies = [String(cookies)]; } if (isArray) { 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 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, util.isSSE(res))); } else if (pattern) { res.addTextTransform(new ReplaceStringTransform(pattern, value, util.isSSE(res))); } }); } function getWriterFile(file, statusCode) { if (!file || statusCode == 200) { return file; } return file + '.' + statusCode; } function readFirstChunk(req, res, src, cb) { var ports = req._pipePluginPorts; if (!cb) { if (ports.reqReadPort || ports.reqWritePort) { delete req.headers['content-length']; } return ports.reqReadPort ? req.getPayload(res, 1) : res(); } if (ports.resReadPort || ports.resWritePort) { delete src.headers['content-length']; } if (!ports.resReadPort) { return cb(); } res.prepareSrc(src, function (stream) { util.readOneChunk(stream, cb); }); } function checkH2(req, isHttps) { if (!config.enableH2) { return; } req.useH2 = req.isH2; var d = req.disable; var e = req.enable; if (isHttps) { if (d.h2 || d.http2 || d.httpsH2) { req.useH2 = false; } else if (e.h2 || e.http2 || e.httpsH2) { req.useH2 = true; } } else { if (d.httpH2) { req.useH2 = false; } else if (e.httpH2) { req.useH2 = true; } req.useHttpH2 = req.useH2; } } module.exports = function (req, res, next) { var origProto; var resRules = req.rules; req.request = function (options) { readFirstChunk(req, function () { options = options || req.options; req.realUrl = res.realUrl = options.isPlugin ? req._realUrl || req.fullUrl : options.href; var originPort = options.port; var originHost = options.host; var originDomain = options.hostname; var now = Date.now(); rules.getClientCert(req, function (key, cert, isPfx, cacheKey) { rules.getProxy( options.href, options.isPlugin ? null : req, function (err, hostIp, hostPort) { var proxyRule = resRules.proxy; var proxyUrl = !options.isPlugin && proxyRule ? util.rule.getMatcher(proxyRule) : null; var headers = req.headers; var auth, isInternalProxy, isHttpsProxy, origPath, origProxy; if (!hostIp) { if (options.localDNS) { req.curUrl = ''; } else if (proxyUrl) { isHttpsProxy = proxyRule.isHttps; isInternalProxy = proxyRule.isInternal || util.isInternalProxy(req); if (isInternalProxy) { req._isInternalProxy = true; if (options.protocol === 'https:') { headers[config.HTTPS_FIELD] = 1; origProto = options.protocol; options.protocol = null; } } else if (proxyRule.isHttp2https) { options.protocol = 'https:'; } req.curUrl = 'http:' + util.removeProtocol(proxyUrl); } else { req.curUrl = options.href; } } if (!req.setServerPort) { req.setServerPort = function (port) { req.serverPort = port; }; } rules.resolveHost(req, function (err, ip, port, hostRule) { var setHostsInfo = function (_ip, _port, _host, withPort) { ip = _ip || '127.0.0.1'; port = _port; req.dnsTime = Date.now() - now; req.hostIp = util.joinIpPort(_ip, withPort && _port); if (_host) { resRules.host = _host; } }; if (proxyUrl && proxyRule && hostRule) { proxyRule.host = hostRule; hostRule = null; } setHostsInfo(hostIp || ip, hostPort || port, hostRule); if (err) { showDnsError(res, err); return; } if (req.disable.keepalive) { req.disable.keepAlive = true; } var isHttps = options.protocol == 'https:'; var proxyOptions, isProxyPort, isSocks; var setAgent = function (disable) { if (disable || req.disable.keepAlive || (isHttps && cert)) { options.agent = false; } else { options.agent = isHttps ? config.httpsAgent : config.httpAgent; } }; checkH2(req, isHttps); if (proxyUrl) { proxyOptions = parseUrl(proxyUrl); proxyOptions.host = ip; proxyOptions.auth = proxyOptions.auth || req._pacAuth; isSocks = proxyRule.isSocks; var proxyPort = proxyOptions.port; if (!proxyPort) { proxyPort = proxyOptions.port = isSocks ? 1080 : isHttpsProxy ? 443 : 80; } if (proxyOptions.auth) { auth = 'Basic ' + util.toBuffer(proxyOptions.auth + '').toString('base64'); } else { auth = headers['proxy-authorization']; } if ( isHttps || (req.useH2 && !isInternalProxy) || isSocks || isHttpsProxy || req._phost ) { isProxyPort = util.isProxyPort(proxyPort); if (isProxyPort && util.isLocalAddress(ip)) { req.setServerPort(config.port); res.response( util.wrapResponse({ statusCode: 302, headers: { location: 'http://' + util.joinIpPort(ip, config.port) + (options.path || '') } }) ); } else { var curServerPort = options.port || (isHttps ? 443 : 80); var proxyHeaders = { host: util.joinIpPort(options.hostname, curServerPort), 'proxy-connection': req.disable.proxyConnection ? 'close' : 'keep-alive' }; pluginMgr.getTunnelKeys().forEach(function (k) { var val = headers[k]; if (val) { proxyHeaders[k] = val; } }); if (auth) { proxyHeaders['proxy-authorization'] = auth; } if (req.disable.proxyUA) { delete proxyHeaders['user-agent']; } else if (headers['user-agent']) { proxyHeaders['user-agent'] = headers['user-agent']; } if (!util.isLocalAddress(req.clientIp)) { proxyHeaders[clientIpKey] = req.clientIp; } if (isHttps || req.useH2) { util.checkIfAddInterceptPolicy(proxyHeaders, headers); } if (isProxyPort) { proxyHeaders[config.WEBUI_HEAD] = 1; } if (util.isProxyPort(curServerPort) || util.isLocalPHost(req, isHttps)) { headers[config.WEBUI_HEAD] = 1; } var clientId = req.headers[config.CLIENT_ID_HEADER]; if (clientId) { proxyHeaders[config.CLIENT_ID_HEADER] = clientId; } util.setClientId( proxyHeaders, req.enable, req.disable, req.clientIp, isInternalProxy ); var phost = req._phost; var opts = { isSocks: isSocks, isHttps: isHttps, _phost: phost, proxyServername: isHttpsProxy ? proxyOptions.hostname : null, proxyHost: ip, clientIp: proxyHeaders[clientIpKey], proxyPort: proxyPort, url: options.href, auth: proxyOptions.auth, headers: proxyHeaders }; if (phost) { options.host = phost.hostname; if (phost.port > 0) { options.port = phost.port; } else if (!options.port) { options.port = isHttps ? 443 : 80; } proxyHeaders.host = util.joinIpPort(options.host, options.port); } else { options.host = options.hostname; } options._proxyOptions = opts; opts.proxyType = isSocks ? 'socks' : isHttpsProxy ? 'https' : 'http'; options._proxyPort = opts.proxyPort; origProxy = opts; request(options); } return; } if (auth) { headers['proxy-authorization'] = auth; } } req.hostIp = util.joinIpPort(ip, port); port = proxyOptions ? proxyOptions.port : port || options.port; options.host = ip; //设置ip var curPort = port || (isHttps ? 443 : 80); isProxyPort = util.isProxyPort(curPort); var isLocalAddress = util.isLocalAddress(options.host); if (isProxyPort && isLocalAddress) { var redirectHost = config.customLocalUIHost || ip; var redirectPort = config.realPort || config.port; res.response( util.wrapResponse({ statusCode: 302, headers: { location: 'http://' + util.joinIpPort(redirectHost, redirectPort) + (options.path || '') } }) ); } else { if ( isProxyPort || util.isProxyPort(options.port || (isHttps ? 443 : 80)) || util.isLocalPHost(req, isHttps) ) { headers[config.WEBUI_HEAD] = 1; } setAgent(isLocalAddress); request(options, port, true); } function request(options, serverPort, direct) { options.headers = headers; options.method = req.method; util.setRejectUnauthorized(req, options); if ( !options.isPlugin && !req._customHost && (req.fullUrl !== req.realUrl || !headers.host) ) { headers.host = originHost; } if (req.disable.keepAlive) { headers.connection = 'close'; } if (direct) { options.port = serverPort; if (proxyUrl) { origPath = options.path || ''; } } delete options.hostname; //防止自动dns delete options.protocol; if (isHttps && !req.disable.servername) { options.servername = util.parseHost(headers.host)[0]; } var piped; var maxRetryCount = 1; var retryCount = 0; var retryXHost = 0; var resetCount = 0; var curClient, timer, newIp; var setProxyAgent = function (options, proxyOpts) { proxyOpts.cacheKey = options.cacheKey; proxyOpts.proxyTunnelPath = util.getProxyTunnelPath( req, isHttps ); proxyOpts.enableIntercept = true; options.agent = proxyOpts.isSocks ? config.getSocksAgent(proxyOpts) : config.getHttpsAgent(proxyOpts, options); }; var retry = function (err) { clearTimeout(timer); timer = null; if (curClient) { curClient.removeListener('error', retry); curClient.removeListener('close', retry); curClient.on('error', util.noop); curClient.destroy(); curClient = null; } if (req._hasError || req._hasRespond) { return; } if (err) { // Chrome 浏览器自动兼容 127.0.0.1 和 ::1 if (util.notStarted(err)) { if (!newIp && (newIp = util.getLocalhostIP(err, req, originDomain, options.host))) { options.host = newIp; setHostsInfo(newIp); } } else if (isHttps && !options.ciphers && util.isCiphersError(err)) { extend(options, util.getTlsOptions(resRules)); return send(); } } if (retryCount >= maxRetryCount) { var toHttp; if ( isHttps && (!piped || req.noReqBody) && util.checkTlsError(err) && util.checkAuto2Http(req, ip, proxyUrl) ) { isHttps = false; toHttp = true; req.httpsTime = req.httpsTime || Date.now(); req.useHttp = true; if (origProxy) { origProxy.isHttps = false; if (req._phost && !req._phost.port) { options.port = 80; origProxy.headers.host = req._phost.hostname + ':80'; } setProxyAgent(options, origProxy); } else { setAgent(util.isLocalAddress(ip)); } } var code = err && err.code; if ( !toHttp && (resetCount > 1 || (code !== 'EPROTO' && code !== 'ECONNRESET') || (piped && !req.noReqBody)) ) { var stack = util.getErrorStack( err || new Error('socket connect timeout') ); res.response(util.wrapGatewayError(stack)); } else { ++resetCount; send(); } return; } ++retryCount; if (proxyUrl) { if (X_RE.test(proxyUrl)) { proxyUrl = ''; req._phost = undefined; if (isInternalProxy) { isHttps = origProto === 'https:'; } req.curUrl = req.realUrl; delete options._proxyPort; rules.resolveHost( req, function (_err, _ip, _port, _host) { setAgent(util.isLocalAddress(_ip)); setHostsInfo(_ip, _port, _host, true); if (_err) { showDnsError(res, _err); return; } options.host = ip; options.port = _port || originPort; ++maxRetryCount; send(); } ); return; } } else if ( retryXHost < 2 && req.rules.host && X_RE.test(req.rules.host.matcher) ) { ++maxRetryCount; ++retryXHost; if (retryXHost > 1) { req.curUrl = req.realUrl; delete options._proxyPort; rules.lookupHost(req, function (_err, _ip) { setHostsInfo(_ip); if (_err) { showDnsError(res, _err); return; } options.host = ip; options.port = originPort; send(); }); return; } } else if ( isHttps && util.checkAuto2Http(req, ip, proxyUrl) ) { ++maxRetryCount; if (maxRetryCount > 2 || util.checkTlsError(err)) { isHttps = false; req.httpsTime = req.httpsTime || Date.now(); req.useHttp = true; setAgent(util.isLocalAddress(options.host)); } } send(); }; var send = function (sock) { if (req._hasError) { return; } req.useH2 = false; req.setServerPort( options._proxyPort || options.port || (isHttps ? 443 : 80) ); if (origPath != null && proxyUrl) { origPath = null; options.path = (isHttps ? 'https:' : 'http:') + '//' + (headers.host || options.host) + (options.path || '/'); } var useHttps = isHttps; if (sock) { options.agent = null; options.createConnection = function () { return sock; }; } else { var proxyOpts = options._proxyOptions; if (proxyOpts) { if (!req.useHttpH2 || proxyOpts._phost) { setProxyAgent(options, proxyOpts); } else { options.host = proxyOpts.proxyHost; options.port = proxyOpts.proxyPort; useHttps = useHttps || isHttpsProxy; } delete options._proxyOptions; } } options.protocol = useHttps ? 'https:' : 'http:'; if (useHttps && !req.disable.secureOptions) { util.setSecureOptions(options); } try { var client = (useHttps ? https : http).request( options, res.response ); curClient = client; req._clientReq = client; client.once('error', retry); client.once('socket', function (socket) { if (socket.connecting || socket._connecting) { if (TIMEOUT) { timer = setTimeout(function () { socket.destroy(); timer = null; retry(); }, TIMEOUT); } socket.once( isHttpsProxy || isHttps ? 'secureConnect' : 'connect', function () { req._connectTime = Date.now(); retryCount = maxRetryCount; piped = true; clearTimeout(timer); timer = null; pipeClient(req, client); } ); } else { retryCount = maxRetryCount; piped = true; pipeClient(req, client); socket.resume(); } }); } catch (e) { res.response( util.wrapGatewayError(util.getErrorStack(e)) ); } }; if (req.disable.clientIp || req.disable.clientIP) { delete headers[clientIpKey]; } else { var forwardedFor = util.getMatcherValue( resRules.forwardedFor ); if (net.isIP(forwardedFor)) { headers[clientIpKey] = forwardedFor; } else if (req._customXFF) { headers[clientIpKey] = req._customXFF; } else if ( (!options.isPlugin && !req.enable.clientIp && !req.enable.clientIP && !req.enableXFF && (isHttps || isSocks || !proxyUrl)) || util.isLocalAddress(req.clientIp) ) { delete headers[clientIpKey]; } else { headers[clientIpKey] = req.clientIp; } } util.deleteReqHeaders(req); var optHeaders = options.headers; var transfer = options.method === 'DELETE' && optHeaders['transfer-encoding']; if (transfer) { delete optHeaders['transfer-encoding']; } var clientId = optHeaders[config.CLIENT_ID_HEADER]; if (clientId) { if (!options.isPlugin && !req._customClientId && !util.isKeepClientId(req, proxyUrl)) { req._origClientId = clientId; util.removeClientId(optHeaders); } req.setClientId && req.setClientId(clientId); } else { util.setClientId( optHeaders, req.enable, req.disable, req.clientIp, isInternalProxy ); } if ( req.useH2 && (isInternalProxy || headers[config.HTTPS_FIELD] || options.isPlugin) ) { headers[config.ALPN_PROTOCOL_HEADER] = 'h2'; } options.headers = optHeaders = formatHeaders( optHeaders, req.rawHeaderNames ); delete headers[config.WEBUI_HEAD]; delete headers[config.HTTPS_FIELD]; delete headers[config.ALPN_PROTOCOL_HEADER]; if (transfer) { optHeaders['Transfer-Encoding'] = transfer; } if (options.isPlugin) { optHeaders[config.PLUGIN_HOOK_NAME_HEADER] = config.PLUGIN_HOOKS.HTTP; } req.noReqBody = !util.hasRequestBody(req); if ( req.method === 'DELETE' && (req._hasInjectBody || req.headers['transfer-encoding'] || req.headers['content-length'] > 0) ) { req.useH2 = false; } req.setServerPort( options._proxyPort || options.port || (isHttps ? 443 : 80) ); req.options = options; isHttps && util.setClientCert(options, key, cert, isPfx, cacheKey); util.addMatchedRules(req); h2.request(req, res, send); } }, req.pluginRules, req.rulesFileMgr, req.headerRulesMgr ); } ); }); }); }; res.response = function (_res) { if (req._hasRespond) { return; } _res.once('readable', function() { req._ttfb = Date.now(); req.setTTFB && req.setTTFB(); }); req._hasRespond = true; res._srcResponse = _res; if (_res.realUrl) { req.realUrl = res.realUrl = _res.realUrl; } var headers = _res.headers; var useFrames; res.headers = req.resHeaders = headers; res._originEncoding = headers['content-encoding']; req.statusCode = _res.statusCode; if (_res.rawHeaderNames) { res.rawHeaderNames = _res.rawHeaderNames; } else { res.rawHeaderNames = _res.rawHeaderNames = Array.isArray(_res.rawHeaders) ? getRawHeaderNames(_res.rawHeaders) : {}; } _res.on('error', function (err) { res.emit('error', err); }); if (!req.isPluginReq && headers[config.PROXY_ID_HEADER] === 'h2') { req.useH2 = true; delete headers[config.PROXY_ID_HEADER]; } if (req.disable.additionalHeaders) { delete headers[util.ADDITIONAL_HEAD]; } util.drain(req, function () { readFirstChunk(req, res, _res, function (firstChunk) { pluginMgr.getResRules(req, _res, function () { var replaceStatus = util.getMatcherValue(resRules.replaceStatus); if (replaceStatus && replaceStatus != _res.statusCode) { res.statusCode = _res.statusCode = replaceStatus; if (!util.isDisableUserLogin(resRules.replaceStatus, req)) { util.handleStatusCode(replaceStatus, headers); } } if (req.disable['301'] && _res.statusCode == 301) { res.statusCode = _res.statusCode = 302; } var resMerge = resRules.resMerge; var ruleList = [ resRules.resHeaders, resRules.resCookies, resRules.resCors, resRules.resReplace, resMerge, resRules.trailers ]; util.parseRuleJson( ruleList, function ( headers, cookies, cors, replacement, params, newTrailers ) { var data = {}; if (headers) { data.headers = extend(data.headers || {}, headers); } if (data.body && typeof data.body !== 'string') { try { data.body = JSON.stringify(data.body); } catch (e) {} } if (data.headers) { data.headers = util.lowerCaseify( data.headers, res.rawHeaderNames ); if (typeof data.headers['content-type'] !== 'string') { delete data.headers['content-type']; } } util.setResCookies(_res, cookies, req); util.setResCors(_res, cors, req); var cache = util.getMatcherValue(resRules.cache); var enable = req.enable; if (cache === 'reserve' || cache === 'keep' || util.isEnable(req, 'keepAllCache')) { req._customCache = true; } else { var maxAge = parseInt(cache, 10); var noCache = NO_CACHE_RE.test(cache) || maxAge < 0; if (maxAge >= 0 || noCache) { req._customCache = true; util.setHeaders(data, { 'cache-control': noCache ? NO_STORE_RE.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="' + util.encodeNonLatin1Char(attachment) + '"' ); } if (resRules.resCharset) { data.charset = util.getMatcherValue(resRules.resCharset); } var resSpeed = util.getMatcherValue(resRules.resSpeed); resSpeed = resSpeed && parseFloat(resSpeed); if (resSpeed > 0) { data.speed = resSpeed; } var _resHeaders = _res.headers; if (data.headers) { setCookies(_resHeaders, data); extend(_resHeaders, data.headers); } if (resRules.resType) { var newType = util.getMatcherValue(resRules.resType).split(';'); var type = newType[0]; newType[0] = !type || type.indexOf('/') != -1 ? type : util.lookupType(type); _resHeaders['content-type'] = util.getNewType(newType.join(';'), _resHeaders); } var delProps = util.parseDelProps(req); util.setCharset(_resHeaders, data.charset, delProps.resType, delProps.resCharset); if (!_resHeaders.pragma) { delete _resHeaders.pragma; } var hr = util.parseHeaderReplace(resRules.headerReplace); util.handleHeaderReplace(_resHeaders, hr.res); if (_resHeaders.location) { //nodejs的url只支持ascii,对非ascii的字符要encodeURIComponent,否则传到浏览器是乱码 _resHeaders.location = util.encodeNonLatin1Char(_resHeaders.location); } var resType = util.getContentType(_resHeaders); var charset = util.getCharset(_resHeaders['content-type']); var isHtml = resType === 'HTML'; var isJs = isHtml || resType === 'JS'; var isCss = isHtml || resType === 'CSS'; var hasResBody = util.hasBody(_res, req); var injectRules = [ resRules.resBody, resRules.resPrepend, resRules.resAppend, isHtml && resRules.htmlAppend, isJs && resRules.jsAppend, isCss && resRules.cssAppend, isHtml && resRules.htmlBody, isJs && resRules.jsBody, isCss && resRules.cssBody, isHtml && resRules.htmlPrepend, isJs && resRules.jsPrepend, isCss && resRules.cssPrepend ]; if (isHtml) { data.isHtml = true; if (util.isEnable(req, 'strictHtml')) { data.strictHtml = true; injectRules.forEach(function(rule) { if (rule) { rule.strictHtml = true; } }); } else if (util.isEnable(req, 'safeHtml')) { data.safeHtml = true; injectRules.forEach(function(rule) { if (rule) { rule.safeHtml = true; } }); } } util.getRuleValue(injectRules, function ( resBody, resPrepend, resAppend, htmlAppend, jsAppend, cssAppend, htmlBody, jsBody, cssBody, htmlPrepend, jsPrepend, cssPrepend ) { if (req._hasError) { return; } if (resBody != null) { data.body = resBody || util.EMPTY_BUFFER; } data.top = resPrepend; data.bottom = resAppend; var speedTransform = data.speed || data.delay ? new SpeedTransform(data) : null; delete data.headers; delete data.speed; delete data.delay; if (isJs || resType === 'JSON' || !_resHeaders['content-type']) { var delBodyProps = util.parseDelResBody(req); var maxResSize = (resMerge && resMerge.lineProps.enableBigData) || util.isEnable(req, 'resMergeBigData') ? BIG_MAX_RES_SIZE : MAX_RES_SIZE; params = util.isEmptyObject(params) ? null : params; if (params || delBodyProps) { var transform = new Transform(); var interrupt; var ctn = ''; transform._transform = function (text, _, callback) { if (text) { if (!interrupt) { ctn += text; text = null; if ( ((isHtml || !_resHeaders['content-type']) && !LIKE_JSON_RE.test(ctn)) || Buffer.byteLength(ctn) > maxResSize ) { interrupt = true; text = ctn; ctn = null; } } } else if (ctn) { text = ctn.replace(JSON_RE, function (json) { var obj = util.parseRawJson(json); if (obj) { if (params) { obj = extend(true, obj, params); } util.deleteProps(obj, delBodyProps); json = JSON.stringify(obj); } return json; }); ctn = null; } else if (!interrupt && params) { util.deleteProps(params, delBodyProps); try { text = JSON.stringify(params); } catch (e) {} } callback(null, text); }; res.addTextTransform(transform); } } var top, body, bottom; if (isHtml) { top = joinArr(data.top, cssPrepend); top = joinArr(top, htmlPrepend); data.top = joinArr(top, jsPrepend); body = joinArr(data.body, cssBody); body = joinArr(body, htmlBody); data.body = joinArr(body, jsBody); bottom = joinArr(data.bottom, cssAppend); bottom = joinArr(bottom, htmlAppend); data.bottom = joinArr(bottom, jsAppend); } else { if (isJs) { top = jsPrepend; body = jsBody; bottom = jsAppend; } else if (isCss) { top = cssPrepend; body = cssBody; bottom = cssAppend; } if (top) { top = util.toBuffer(top, charset); data.top = data.top ? Buffer.concat([data.top, CRLF, top]) : top; } if (body) { body = util.toBuffer(body, charset); data.body = data.body ? Buffer.concat([data.body, CRLF, body]) : body; } if (bottom) { bottom = util.toBuffer(bottom, charset); data.bottom = data.bottom ? Buffer.concat([data.bottom, CRLF, bottom]) : bottom; } } if (data.body || data.top || data.bottom) { !util.isEnable(req, 'keepAllCSP') && !util.isEnable(req, 'keepCSP') && util.disableCSP(_resHeaders); !req._customCache && !util.isEnable(req, 'keepCache') && util.disableResStore(_resHeaders); } if (!hasResBody) { delete data.speed; delete data.body; delete data.top; delete data.bottom; } else { util.removeResBody(req, data); } if (util.isWhistleTransformData(data)) { data.noDoctype = util.isDisable(req, 'doctype'); res.addZipTransform(new WhistleTransform(data)); } if (hasResBody) { handleReplace(res, replacement); } //一定放在最后,确保能过滤到动态注入的内容 if (speedTransform) { res.add(speedTransform); } var bodyFile = hasResBody ? getWriterFile( util.getWriteFilePath(resRules.resWrite), _res.statusCode ) : null; var rawFile = getWriterFile( util.getWriteFilePath(resRules.resWriteRaw), _res.statusCode ); util.getFileWriters( [bodyFile, rawFile], function (writer, rawWriter) { if (req._hasError) { return; } res.on('src', function (_res) { if (writer) { res.addZipTransform( new FileWriterTransform(writer, _res) ); } if (rawWriter) { res.addZipTransform( new FileWriterTransform( rawWriter, _res, true, req ) ); } }); var resHeaders = delProps.resHeaders; if (resHeaders) { Object.keys(resHeaders).forEach(function (prop) { delete _resHeaders[prop]; }); } if (_resHeaders[config.ALPN_PROTOCOL_HEADER] === 'h2') { req.useH2 = true; } util.delay( util.getMatcherValue(resRules.resDelay), function () { if (req._hasError) { return; } if (util.needAbortRes(req)) { req.__resHeaders = _res.headers; req.__statusCode = _res.statusCode; return res.destroy(); } res.src(_res, null, firstChunk); firstChunk = null; var rawNames = res.rawHeaderNames || {}; var encoding = util.getEnableEncoding(enable); if (encoding) { rawNames['content-encoding'] = rawNames['content-encoding'] || 'Content-Encoding'; _resHeaders['content-encoding'] = encoding; delete _resHeaders['content-length']; } else if ( req._pipePluginPorts.resReadPort || req._pipePluginPorts.resWritePort ) { delete req.headers['content-length']; } util.disableResProps(req, _resHeaders); if (req._filters.showHost || enable.showHost) { _resHeaders['x-host-ip'] = req.hostIp || LOCALHOST; } util.setResponseFor( resRules, _resHeaders, req, req.hostIp, req._phost ); pluginMgr.postStats(req, res); if ( !hasResBody && _resHeaders['content-length'] > 0 && !util.isHead(req) ) { delete _resHeaders['content-length']; } if (!req.disable.trailerHeader) { util.addTrailerNames( _res, newTrailers, rawNames, delProps.trailers, req ); } if (req.enableCustomParser) { if ( _res.isCustomRes || _resHeaders['x-whistle-disable-custom-frames'] ) { delete _resHeaders[ 'x-whistle-disable-custom-frames' ]; req.disableCustomParser(); } else { useFrames = true; req.enableCustomParser(_res); } } var curHeaders = _resHeaders; if (req.fromComposer) { curHeaders = extend({}, _resHeaders); curHeaders['x-whistle-req-id'] = req.reqId; util.setFramesMode(curHeaders, useFrames); } util.addMatchedRules(req, _res); try { res.writeHead( _res.statusCode, formatHeaders(curHeaders, rawNames) ); util.onResEnd(_res, function () { var trailers = _res.trailers; if ( !res.chunkedEncoding || req.disable.trailers || req.disable.trailer || (util.isEmptyObject(trailers) && util.isEmptyObject(newTrailers)) ) { return; } var rawHeaderNames = _res.rawTrailers ? getRawHeaderNames(_res.rawTrailers) : {}; if (newTrailers) { newTrailers = util.lowerCaseify( newTrailers, rawHeaderNames ); if (trailers) { extend(trailers, newTrailers); } else { trailers = newTrailers; } } var delTrailers = delProps.trailers; if (delTrailers) { Object.keys(delTrailers).forEach(function (prop) { delete trailers[prop]; }); } util.handleHeaderReplace(trailers, hr.trailer); res.setCurTrailers && res.setCurTrailers(trailers, rawHeaderNames);