UNPKG

whistle

Version:

HTTP, HTTPS, Websocket debugging proxy

1,458 lines (1,208 loc) 34.6 kB
var http = require('http'); var url = require('url'); var path = require('path'); var os = require('os'); var fs = require('fs'); var vm = require('vm'); var EventEmitter = require('events').EventEmitter; var fse = require('fs-extra'); var qs = require('querystring'); var extend = require('util')._extend; var StringDecoder = require('string_decoder').StringDecoder; var PassThrough = require('stream').PassThrough; var iconv = require('iconv-lite'); var zlib = require('zlib'); var PipeStream = require('pipestream'); var mime = require('mime'); var Q = require('q'); var protoMgr = require('../rules/protocols'); var logger = require('./logger'); var config = require('../config'); var fileWriterCache = {}; var CRLF_RE = /\r\n|\r|\n/g; var UTF8_OPTIONS = {encoding: 'utf8'}; var MAX_RULES_SIZE = 1024 * 1024 * 256; var REQUEST_TIMEOUT = 5000; var LOCALHOST = '127.0.0.1'; exports.WhistleTransform = require('./whistle-transform'); exports.ReplacePatternTransform = require('./replace-pattern-transform'); exports.ReplaceStringTransform = require('./replace-string-transform'); exports.SpeedTransform = require('./speed-transform'); exports.FileWriterTransform = require('./file-writer-transform'); exports.MultiPartParser = require('./multipart-parser'); exports.parseReq = require('./parse-req'); exports.getServer = require('./get-server'); exports.formatHeaders = exports.parseReq.formatHeaders; exports.getRawHeaderNames = exports.parseReq.getRawHeaderNames; function noop() {} exports.noop = noop; function listenerCount(emitter, eventName) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(eventName); } return EventEmitter.listenerCount(emitter, eventName); } exports.listenerCount = listenerCount; function getEventTransform(proxy, eventName, url) { if (!listenerCount(proxy, eventName)) { return; } var transform = new PassThrough(); transform.on('error', noop); transform._transform = function(chunk, encoding, cb) { proxy.emit(eventName, url); cb(null, chunk); }; return transform; } exports.getEventTransform = getEventTransform; function changePort(url, port) { var index = url.indexOf('/', url.indexOf('://') + 3); if (index != -1) { var host = url.substring(0, index).replace(/:\d*$/, ''); url = host + ':' + port + url.substring(index); } return url; } exports.changePort = changePort; function handleStatusCode(statusCode, headers) { if (statusCode == 401) { headers['www-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; var scriptCache = {}; var VM_OPTIONS = { displayErrors: false, timeout: 100 }; 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 a.time > b.time ? -1 : 1; }).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(content) }; script.time = Date.now(); return script.script; } function execScriptSync(script, context) { try { if (script = getScript(script)) { context.console = {}; ['fatal', 'error', 'warn', 'info', 'log', 'debug'] .forEach(function(level) { context.console[level] = logger[level]; }); script.runInNewContext(context, VM_OPTIONS); } return true; } catch(e) { logger.error(e); } } exports.execScriptSync = execScriptSync; function toMultipart(name, value) { if (value == null) { value = ''; } if (typeof value == 'object') { var filename = value.filename || value.name; filename = filename == null ? '' : filename + ''; value = value.content || value.value || ''; return toBuffer('Content-Disposition: form-data; name="' + name + '"; filename="' + filename + '"\r\nContent-Type: ' + mime.lookup(filename) + '\r\n\r\n' + value); } return toBuffer('Content-Disposition: form-data; name="' + name + '"\r\n\r\n' + value); } exports.toMultipart = toMultipart; function toMultiparts(params, boundary) { var content = Object.keys(params).map(function(name) { return boundary + '\r\n' + toMultipart(name, params[name]); }).join('\r\n'); return toBuffer(content ? '\r\n' + content : ''); } exports.toMultiparts = toMultiparts; function getFileWriter(file, callback) { if (!file || fileWriterCache[file]) { return callback(); } var execCallback = function(writer) { delete fileWriterCache[file]; callback(writer); }; fs.stat(file, function(err, stat) { if (!err) { return execCallback(); } logger.warn(err); fse.ensureFile(file, function(err) { execCallback(err ? null : fs.createWriteStream(file).on('error', logger.error)); logger.error(err); }); }); } exports.getFileWriter = getFileWriter; function getFileWriters(files, callback) { if (!Array.isArray(files)) { files = [files]; } Q.all(files.map(function(file) { var defer = Q.defer(); getFileWriter(file, function(writer) { defer.resolve(writer); }); return defer.promise; })).spread(callback); } exports.getFileWriters = getFileWriters; function clone(obj) { if (!obj || typeof obj != 'object') { return obj; } var result = Array.isArray(obj) ? [] : {}; Object.keys(obj).forEach(function(name) { result[name] = clone(obj[name]); }); return result; } exports.clone = clone; function toBuffer(buf, charset) { if (buf == null || buf instanceof Buffer) { return buf; } buf += ''; if (typeof charset === 'string') { charset = charset.toLowerCase().trim(); if (charset && charset !== 'utf8' && charset !== 'utf-8') { try { return iconv.encode(buf, charset); } catch (e) {} } } return new Buffer(buf); } exports.toBuffer = toBuffer; exports.mkdir = function mkdir(path) { !fs.existsSync(path) && fs.mkdirSync(path); return path; }; function getErrorStack(err) { if (!err) { return ''; } var stack; try { stack = err.stack; } catch(e) {} return 'From: ' + config.name + '\r\nDate: ' + formatDate() + '\r\n' + (stack || err.message || err); } exports.getErrorStack = getErrorStack; function formatDate(now) { now = now || new Date(); return now.toLocaleString(); } exports.formatDate = formatDate; var REG_EXP_RE = /^\/(.+)\/(i)?$/; exports.isRegExp = function isRegExp(regExp) { return REG_EXP_RE.test(regExp); }; var ORIG_REG_EXP = /^\/(.+)\/([igm]{0,3})$/; exports.isOriginalRegExp = function isOriginalRegExp(regExp) { if (!ORIG_REG_EXP.test(regExp) || /[igm]{2}/.test(regExp.$2)) { return false; } return true; }; exports.toOriginalRegExp = function toRegExp(regExp) { regExp = ORIG_REG_EXP.test(regExp); try { regExp = regExp && new RegExp(RegExp.$1, RegExp.$2); } catch(e) { regExp = null; } return regExp; }; exports.emitError = function(obj, err) { if (obj) { obj.once('error', noop); obj.emit('error', err || new Error('Unknown')); } }; exports.indexOfList = function(buf, subBuf, start) { start = start || 0; if (buf.indexOf) { return buf.indexOf(subBuf, start); } var subLen = subBuf.length; if (subLen) { for (var i = start, len = buf.length - subLen; i <= len; i++) { var j = 0; for (; j < subLen; j++) { if (subBuf[j] !== buf[i + j]) { break; } } if (j == subLen) { return i; } } } return -1; }; 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; }; exports.getHost = function parseHost(_url) { _url = url.parse(setProtocol(_url || '')).hostname; return _url && _url.toLowerCase(); }; exports.setTimeout = function(callback) { var timeout = parseInt(config.timeout, 10); return setTimeout(callback, timeout > 0 ? timeout : 36000); }; exports.toRegExp = function toRegExp(regExp) { regExp = REG_EXP_RE.test(regExp); try { regExp = regExp && new RegExp(RegExp.$1, RegExp.$2); } catch(e) { regExp = null; } return regExp; }; function getFullUrl(req) { var host = req.headers.host; if (hasProtocol(req.url)) { var options = url.parse(req.url); req.url = options.path; if (options.host) { host = req.headers.host = options.host; } } if (host) { host = req.isHttps ? host.replace(/:443$/, '') : host.replace(/:80$/, ''); } return _getProtocol(req.isHttps) + host + req.url; } exports.getFullUrl = getFullUrl; function setProtocol(url, isHttps) { return hasProtocol(url) ? url : _getProtocol(isHttps) + url; } function _getProtocol(isHttps) { return isHttps ? 'https://' : 'http://'; } function hasProtocol(url) { return /^[a-z0-9.+-]+:\/\//i.test(url); } function getProtocol(url) { return hasProtocol(url) ? url.substring(0, url.indexOf('://') + 1) : null; } function removeProtocol(url, clear) { return hasProtocol(url) ? url.substring(url.indexOf('://') + (clear ? 3 : 1)) : url; } function replaceProtocol(url, protocol) { return (protocol || 'http:') + removeProtocol(url); } exports.hasProtocol = hasProtocol; exports.setProtocol = setProtocol; exports.getProtocol = getProtocol; exports.removeProtocol = removeProtocol; exports.replaceProtocol = replaceProtocol; 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(); (function updateSystyemInfo () { interfaces = os.networkInterfaces(); hostname = os.hostname(); setTimeout(updateSystyemInfo, 30000); })(); function networkInterfaces() { return interfaces; } function getHostname() { return hostname; } exports.networkInterfaces = networkInterfaces; exports.hostname = getHostname; function isLocalAddress(address) { if (!address || typeof address != 'string') { return false; } if (address == LOCALHOST || address == '0:0:0:0:0:0:0:1') { return true; } address = address.toLowerCase(); interfaces = networkInterfaces(); for (var i in interfaces) { var list = interfaces[i]; if (Array.isArray(list)) { for (var j = 0, info; info = list[j]; j++) { if (info.address.toLowerCase() == address) { return true; } } } } return false; } exports.isLocalAddress = isLocalAddress; exports.drain = function drain(stream, endHandler) { var emitEndStream = new PassThrough(); emitEndStream.on('data', noop).on('error', noop); typeof endHandler == 'function' && emitEndStream.on('end', endHandler); stream.pipe(emitEndStream); }; exports.encodeNonAsciiChar = function encodeNonAsciiChar(str) { if (!str || typeof str != 'string') { return ''; } /*eslint no-control-regex: "off"*/ return str && str.replace(/[^\x00-\x7F]/g, safeEncodeURIComponent); }; /** * 解析一些字符时,encodeURIComponent可能会抛异常,对这种字符不做任何处理 * http://stackoverflow.com/questions/16868415/encodeuricomponent-throws-an-exception * @param ch * @returns */ function safeEncodeURIComponent(ch) { try { return encodeURIComponent(ch); } catch(e) {} return ch; } exports.encodeURIComponent = safeEncodeURIComponent; function getPath(url, noProtocol) { if (url) { url = url.replace(/\/?[?#].*$/, ''); 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 wrapResponse(res) { var passThrough = new PassThrough(); passThrough.statusCode = res.statusCode; passThrough.rawHeaderNames = res.rawHeaderNames; passThrough.headers = lowerCaseify(res.headers); passThrough.trailers = lowerCaseify(res.trailers); passThrough.headers.server = config.name; res.body != null && passThrough.push(String(res.body)); passThrough.push(null); return passThrough; } exports.wrapResponse = wrapResponse; function wrapGatewayError(body) { return wrapResponse({ statusCode: 502, headers: { 'content-type': 'text/html; charset=utf8' }, body: body ? '<pre>' + body + '</pre>' : '' }); } exports.wrapGatewayError = wrapGatewayError; function findArray(arr, cb) { if (typeof arr.find === 'function') { return arr.find(cb); } for (var i = 0, len = arr.length; i < len; i++) { var val = arr[i]; if (cb(val, i, arr)) { return val; } } } exports.findArray = findArray; 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; function parseInlineJSON(text, isValue) { if (!text || typeof text != 'string' || /\s/.test(text) || (!isValue && (/\\|\//.test(text) && !/^&/.test(text)))) { return; } return qs.parse(text); } function parseLinesJSON(text) { if (!text || typeof text != 'string' || !(text = text.trim()) || /^[\{\[]/.test(text)) { return null; } var obj = {}; text.split(/\r\n|\n|\r/g).forEach(function(line) { if (line = line.trim()) { var index = line.indexOf(': '); var name, value; if (index != -1) { name = line.substring(0, index).trim(); value = line.substring(index + 2).trim(); } else { name = line.trim(); value = ''; } var list = obj[name]; if (list == null) { obj[name] = value; } else { if (!Array.isArray(list)) { obj[name] = list = [list]; } list.push(value); } } }); return obj; } function parseJSON(data) { return _parseJSON(data, true) || parseLinesJSON(data); } function _parseJSON(data, isValue) { if (typeof data != 'string' || !(data = data.trim())) { return null; } try { return JSON.parse(data); } catch(e) { logger.error(e); } return parseInlineJSON(data, isValue); } exports.parseJSON = parseJSON; function readFiles(files, callback) { if (!Array.isArray(files)) { files = [files]; } Q.all(files.map(function(file) { var defer = Q.defer(); if (file && typeof file == 'string') { fs.readFile(file, UTF8_OPTIONS, function(err, data) { defer.resolve(err ? null : data); logger.error(err); }); } else { defer.resolve(); } return defer.promise; })).spread(callback); } exports.readFiles = readFiles; function readFileSync(file) { try { return fs.readFileSync(file, UTF8_OPTIONS); } catch(e) {} } exports.readFileSync = readFileSync; function trim(text) { return text && text.trim(); } exports.trim = trim; function readInjectFiles(data, callback) { if (!data) { return callback(); } readFiles([data.prepend, data.replace, data.append], function(top, body, bottom) { if (top != null) { data.top = top; } if (body != null) { data.body = body; } if (bottom != null) { data.bottom = bottom; } callback(data); }); } exports.readInjectFiles = readInjectFiles; function lowerCaseify(obj, rawNames) { var result = {}; if (!obj) { return result; } Object.keys(obj).forEach(function(name) { var value = obj[name]; if (value !== undefined) { var key = name.toLowerCase(); result[key] = Array.isArray(value) ? value : value + ''; if (rawNames) { rawNames[key] = name; } } }); return result; } exports.lowerCaseify = lowerCaseify; function parseHeaders(headers, rawNames) { if (typeof headers == 'string') { headers = headers.split(CRLF_RE); } var _headers = {}; headers.forEach(function(line) { var index = line.indexOf(':'); var value; if (index != -1 && (value = line.substring(index + 1).trim())) { var rawName = line.substring(0, index).trim(); var name = rawName.toLowerCase(); var list = _headers[name]; if (rawNames) { rawNames[name] = rawName; } if (list) { if (!Array.isArray(list)) { _headers[name] = list = [list]; } list.push(value); } else { _headers[name] = value; } } }); return lowerCaseify(_headers); } exports.parseHeaders = parseHeaders; function parseRuleJson(rules, callback) { if (!Array.isArray(rules)) { rules = [rules]; } Q.all(rules.map(function(rule) { var defer = Q.defer(); var json = _parseJSON(rule && removeProtocol(getMatcher(rule), true)); if (json) { defer.resolve(json); } else { _getRuleValue(rule, function(data) { defer.resolve(parseJSON(data)); }); } return defer.promise; })).spread(callback); } exports.parseRuleJson = parseRuleJson; function _getRuleValue(rule, callback) { if (!rule) { return callback(); } if (rule.value) { return callback(removeProtocol(rule.value, true)); } var filePath = decodePath(getPath(getMatcher(rule))); if (rule.root) { filePath = join(rule.root, filePath); } protoMgr.isBinProtocol(rule.name) ? fs.readFile(filePath, function(err, data) { callback(err ? null : data); logger.error(err); }) : fs.readFile(filePath, UTF8_OPTIONS, function(err, data) { callback(err ? null : data); logger.error(err); }); } function getRuleValue(rules, callback, noBody) { if (noBody) { return callback(); } if (!Array.isArray(rules)) { rules = [rules]; } Q.all(rules.map(function(rule) { var defer = Q.defer(); _getRuleValue(rule, function(data) { defer.resolve(data); }); return defer.promise; })).spread(callback); } exports.getRuleValue = getRuleValue; 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; } function getRuleFiles(rule) { var files = rule.files || [getPath(getUrl(rule))]; var root = rule.root; return files.map(function(file) { return root ? join(root, decodePath(file)) : decodePath(file); }); } exports.getRuleFiles = getRuleFiles; function getRuleFile(rule) { var filePath = getPath(getUrl(rule)); if (!filePath) { return filePath; } return rule.root ? join(rule.root, decodePath(filePath)) : decodePath(filePath); } exports.getRuleFile = getRuleFile; function getValue(rule) { return rule.value || rule.path; } function getMatcher(rule) { return rule && (getValue(rule) || rule.matcher); } function getUrl(rule) { return rule && (getValue(rule) || rule.url); } exports.rule = { getMatcher: getMatcher, getUrl: getUrl }; function getMatcherValue(rule) { rule = getMatcher(rule); return rule && removeProtocol(rule, true); } exports.getMatcherValue = getMatcherValue; function getContentType(contentType) { if (contentType && typeof contentType != 'string') { contentType = contentType['content-type'] || contentType.contentType; } if (typeof contentType == 'string') { contentType = contentType.toLowerCase(); 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'; } } return null; } exports.getContentType = getContentType; function supportHtmlTransform(res) { var headers = res.headers; if (getContentType(headers) != 'HTML' || !hasBody(res)) { return false; } var contentEncoding = getContentEncoding(headers); //chrome新增了sdch压缩算法,对此类响应无法解码,deflate无法区分deflate还是deflateRaw return !contentEncoding || contentEncoding == 'gzip'; } exports.supportHtmlTransform = supportHtmlTransform; function removeUnsupportsHeaders(headers, supportsDeflate) {//只保留支持的zip格式:gzip、deflate if (!headers || !headers['accept-encoding']) { return; } var list = headers['accept-encoding'].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')) { acceptEncoding.push(ae); } } if (acceptEncoding = acceptEncoding.join(', ')) { headers['accept-encoding'] = acceptEncoding; } else { delete headers['accept-encoding']; } } exports.removeUnsupportsHeaders = removeUnsupportsHeaders; function hasBody(res) { var statusCode = res.statusCode; return !(statusCode == 204 || (statusCode >= 300 && statusCode < 400) || (100 <= statusCode && statusCode <= 199)); } exports.hasBody = hasBody; function hasRequestBody(req) { req = typeof req == 'string' ? req : req.method; if (typeof req != 'string') { return false; } req = req.toUpperCase(); return !(req === 'GET' || req === 'HEAD' || req === 'DELETE' || req === 'OPTIONS' || req === 'CONNECT'); } exports.hasRequestBody = hasRequestBody; function getPipeZipStream(headers) { var pipeStream = new PipeStream(); switch (getContentEncoding(headers)) { case 'gzip': pipeStream.addHead(zlib.createGunzip()); pipeStream.addTail(zlib.createGzip()); break; case 'deflate': pipeStream.addHead(zlib.createInflate()); pipeStream.addTail(zlib.createDeflate()); break; } return pipeStream; } exports.getPipeZipStream = getPipeZipStream; function getContentEncoding(headers) { var encoding = toLowerCase(headers && headers['content-encoding']); return encoding === 'gzip' || encoding === 'deflate' ? encoding : null; } exports.getContentEncoding = getContentEncoding; function isEmptyObject(obj) { return !obj || !Object.keys(obj).length; } exports.isEmptyObject = isEmptyObject; function getPipeIconvStream(headers, plainText) { 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; var decoder = new StringDecoder(); var content = ''; res.on('data', function(chunk) { buffer = buffer ? Buffer.concat([buffer, chunk]) : chunk; if (!charset) { try { content += chunk ? decoder.write(chunk) : decoder.end(); } catch(e) {//可能抛出异常RangeError: out of range index,具体原因未知 content = (buffer || '') + ''; logger.error(e); } if (!plainText) {//如果没charset charset = getMetaCharset(content); } } resolveCharset(buffer); }); res.on('end', resolveCharset); function resolveCharset(chunk) { if (!charset && (!chunk || content.length >= 51200)) { charset = content.indexOf('�') != -1 ? 'gbk' : 'utf8'; content = null; } if (!charset) { return; } 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 toLowerCase(str) { return typeof str == 'string' ? str.trim().toLowerCase() : str; } exports.toLowerCase = toLowerCase; function toUpperCase(str) { return typeof str == 'string' ? str.trim().toUpperCase() : str; } exports.toUpperCase = toUpperCase; var CHARSET_RE = /charset=([\w-]+)/i; var META_CHARSET_RE = /<meta\s[^>]*\bcharset=(?:'|")?([\w-]+)[^>]*>/i; function getCharset(str) { return _getCharset(str); } function getMetaCharset(str) { return _getCharset(str, true); } function _getCharset(str, isMeta) { var charset; if ((isMeta ? META_CHARSET_RE : CHARSET_RE).test(str)) { charset = RegExp.$1; if (!iconv.encodingExists(charset)) { charset = null; } } return charset; } exports.getCharset = getCharset; exports.getMetaCharset = getMetaCharset; function getClientIp(req, forwarded) { var ip; var headers = req.headers || {}; try { ip = (forwarded && headers['x-forwarded-for']) || headers[config.CLIENT_IP_HEAD] || req.connection.remoteAddress || req.socket.remoteAddress; } catch(e) {} return removeIPV6Prefix(ip); } exports.getClientIp = getClientIp; function removeIPV6Prefix(ip) { if (typeof ip != 'string') { return ip; } return ip.indexOf('::ffff:') === 0 ? ip.substring(7) : ip; } exports.removeIPV6Prefix = removeIPV6Prefix; function isUrlEncoded(req) { return /^post$/i.test(req.method) && /urlencoded/i.test(req.headers && req.headers['content-type']); } exports.isUrlEncoded = isUrlEncoded; function isMultipart(req) { return /multipart/i.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) { if (replaceQuery && typeof replaceQuery != 'string') { replaceQuery = qs.stringify(replaceQuery); } if (!query || !replaceQuery) { return query || replaceQuery; } var queryList = []; var params = {}; query = query.split('&').map(filterName); replaceQuery = replaceQuery.split('&').map(filterName); query.concat(replaceQuery).forEach(function(name) { if (name) { var value = params[name]; queryList.push(name + (value == null ? '' : '=' + value)); } }); function filterName(param) { var index = param.indexOf('='); var name, value; if (index == -1) { name = param; value = null; } else { name = param.substring(0, index); value = param.substring(index + 1); } var exists = name in params; params[name] = value; return exists ? null : name; } return queryList.join('&'); } exports.replaceQueryString = replaceQueryString; function replaceUrlQueryString(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.replaceUrlQueryString = replaceUrlQueryString; function decodeBuffer(buffer) { if (!buffer) { return ''; } var text = buffer + ''; return text.indexOf('�') == -1 ? text : iconv.decode(buffer, 'GB18030'); } exports.decodeBuffer = decodeBuffer; function setHeaders(data, obj) { data.headers = data.headers || {}; for (var i in obj) { data.headers[i] = obj[i]; } return data; } exports.setHeaders = setHeaders; function setHeader(data, name, value) { data.headers = data.headers || {}; data.headers[name] = value; return data; } exports.setHeader = setHeader; function getResponseBody(options, callback) { var done; var body = ''; var callbackHandler = function(err) { clearTimeout(timer); if (!done) { done = true; callback(err, body); } err && client.abort(); }; var timeoutHandler = function() { callbackHandler(new Error('Timeout')); }; var timer = setTimeout(timeoutHandler, REQUEST_TIMEOUT); var client = http.get(options, function(res) { res.on('error', callbackHandler); res.setEncoding('utf8'); res.on('data', function(data) { body += data; if (body.length > MAX_RULES_SIZE) { client.abort(); callbackHandler(); } else { clearTimeout(timer); timer = setTimeout(timeoutHandler, REQUEST_TIMEOUT); } }); res.on('end', callbackHandler); }); client.on('error', callbackHandler); client.end(); } exports.getResponseBody = getResponseBody; function join(root, dir) { return root ? path.resolve(root, dir) : dir; } exports.join = join; function mergeRuleProps(origin, add) { if (origin) { origin.list = origin.list || [extend({}, origin)]; } if (add) { add.list = add.list || [extend({}, add)]; } if (origin && add) { origin.list = add.list.concat(origin.list); } } function resolveRuleProps(rule, result) { result = result || {}; if (rule) { rule.list.forEach(function(rule) { getMatcherValue(rule) .split('|') .forEach(function(action) { result[action] = true; }); }); } return result; } var PLUGIN_RE = /^(?:plugin|whistle)\.[a-z\d_\-]+$/; var enableRules = ['https', 'intercept', 'hide']; function ignorePlugins(rules, name) { if (!rules.plugin || !PLUGIN_RE.test(name)) { return; } var list = rules.plugin.list; var protocol = name + ':'; for (var i = list.length - 1; i >= 0; i--) { if (list[i].matcher.indexOf(protocol) === 0) { list.splice(i, 1); } } if (!list.length) { delete rules.plugin; } return true; } function ignoreRules(rules, ignore, isResRules) { enableRules.forEach(function(name) { delete ignore[name]; }); delete ignore.http; delete ignore.filter; Object.keys(ignore).forEach(function(name) { if (!isResRules || protoMgr.resProtocols.indexOf(name) != -1) { if (ignorePlugins(rules, name)) { return; } var rule = rules[name]; var isProxy = name === 'socks' || name === 'proxy'; if (!rule || isProxy) { rule = isProxy ? rules.proxy : rules.rule; if (!rule || rule.matcher.indexOf(name + ':') !== 0) { return; } name = isProxy ? 'proxy' : 'rule'; } delete rules[name]; } }); } exports.ignoreRules = ignoreRules; function mergeRules(req, add, isResRules) { var origin = req.rules; var origAdd = add; add = add || {}; mergeRuleProps(add['delete'], origin['delete']); mergeRuleProps(add.filter, origin.filter); mergeRuleProps(add.disable, origin.disable); mergeRuleProps(add.ignore, origin.ignore); mergeRuleProps(add.enable, origin.enable); if (isResRules && origAdd) { protoMgr.resProtocols.forEach(function(protocol) { var rule = add[protocol]; if (rule) { origin[protocol] = rule; } }); } else { extend(origin, origAdd); } req['delete'] = resolveRuleProps(origin['delete'], req['delete']); req.filter = resolveRuleProps(origin.filter, req.filter); req.disable = resolveRuleProps(origin.disable, req.disable); req.ignore = resolveRuleProps(origin.ignore, req.ignore); req.enable = resolveRuleProps(origin.enable, req.enable); enableRules.forEach(function(rule) { if (req.enable[rule]) { req.filter[rule] = true; } }); ignoreRules(origin, extend(req.ignore, req.filter), isResRules); return add; } exports.mergeRules = mergeRules; function transformReq(req, res, port, weinre) { var options = url.parse(getFullUrl(req)); options.host = LOCALHOST; options.method = req.method; options.hostname = null; options.protocol = null; if (port > 0) { options.port = port; } if(req.clientIp || !req.headers['x-forwarded-for']) { req.headers['x-forwarded-for'] = req.clientIp || getClientIp(req) || LOCALHOST; } options.headers = req.headers; var client = http.request(options, function(_res) { if (weinre && options.pathname == '/target/target-script-min.js') { _res.headers['access-control-allow-origin'] = '*'; } if (getStatusCode(_res.statusCode)) { res.writeHead(_res.statusCode, _res.headers); _res.pipe(res); _res.trailers && res.addTrailers(_res.trailers); } else { sendStatusCodeError(res, _res); } }); client.on('error', function(err) { res.emit('error', err); }); req.pipe(client); return client; } exports.transformReq = transformReq;