repo_description: HTTP, HTTP2, HTTPS, Websocket debugging proxy
repo_topics: charles debug fiddler hosts node nodejs proxy web weinre
code: var path = require('path');
  var http = require('http');
  var fs = require('fs');
  var net = require('net');
  var dns = require('dns');
  var qs = require('querystring');
  var LRU = require('lru-cache');
  var httpAgent = http.Agent;
  var httpsAgent = require('https').Agent;
  var parseUrl = require('./util/parse-url-safe');
  var fse = require('fs-extra2');
  var extend = require('extend');
  var tunnel = require('hagent').agent;
  var socks = require('sockx');
  var json5 = require('json5');
  var pkgConf = require('../package.json');
  var config = extend(exports, pkgConf);
  var common = require('./util/common');
  
  var customUIHost;
  var customHostPluginMap = {};
  var customPluginNameHost = {};
  var WHISTLE_PLUGIN_RE = /^(?:whistle\.)?([a-z\d_\-]+)$/;
  var CMD_RE = /^[\w]{1,12}(?:\s+-g)?$/;
  var httpsAgents = new LRU({ max: 360 });
  var socksAgents = new LRU({ max: 100 });
  var version = process.version.substring(1).split('.');
  var disableAgent = version[0] > 10;
  var useVerbatim = version[0] >= 17;
  var hasIPv6First = version[0] >= 22 && version[1] >= 1;
  var hasDnsOrder = typeof dns.setDefaultResultOrder === 'function';
  var uid = Date.now().toString(16) + (process.pid * 10000 + Math.floor(Math.random() * 10000)).toString(16);
  var IPV4_RE = /^([\d.]+)(:\d{1,5})?$/;
  var IPV6_RE = /^\[([\w:]+)\](:\d{1,5})?$/;
  var noop = function () {};
  var DATA_KEY_RE = /^(clientip|clientid|tunneldata)([=.])([\w.-]+)$/i;
  var DNS_ORDERS = ['ipv4First', 'ipv4first', 'ipv6first', 'ipv6First', 'verbatim'];
  var LOCAL_UI_HOST_LIST = [
    'local.whistlejs.com',
    'local.wproxy.org',
    'rootca.pro'
  ];
  var variableProperties = [
    'encrypted',
    'sockets',
    'dataDirname',
    'storage',
    'baseDir',
    'noGlobalPlugins',
    'pluginsDataMap',
    'globalData',
    'username',
    'password',
    'debugMode',
    'localUIHost',
    'extra',
    'rules',
    'values',
    'dnsCache',
    'allowDisableShadowRules',
    'disableInstaller'
  ];
  if (hasDnsOrder) {
    if (hasIPv6First) {
      config.verbatim = 2;
    } else {
      config.verbatim = 1;
    }
  }
  
  config.appName = common.upperFirst(config.name);
  config.defaultDnsOrder = useVerbatim ? 1 : 2;
  config.uid = uid;
  config.rejectUnauthorized = false;
  config.enableH2 = version[0] > 12; // 支持 HTTP2 要求的最低 Node 版本
  config.ASSESTS_PATH = path.join(__dirname, '../assets');
  config.WHISTLE_POLICY_HEADER = 'x-whistle-policy';
  config.CLIENT_IP_HEADER = common.CLIENT_IP_HEADER;
  config.HTTPS_FIELD = common.HTTPS_FIELD;
  config.CLIENT_INFO_HEADER = 'x-whistle-client-info-' + uid;
  config.SHOW_LOGIN_BOX = 'x-whistle-show-login-box-' + uid;
  config.WEBUI_PATH = '/.whistle-path.5b6af7b9884e1165/';
  config.PREVIEW_PATH_RE = /\?\?\?WHISTLE_PREVIEW_CHARSET=([A-Z\d_-]+)\?\?\?$/;
  config.PLUGIN_HOOK_NAME_HEADER = 'x-whistle-plugin-hook-name_';
  config.CLIENT_ID_HEADER = common.CLIENT_ID_HEADER;
  config.CLIENT_PORT_HEADER = common.CLIENT_PORT_HEADER;
  config.ALPN_PROTOCOL_HEADER = common.ALPN_PROTOCOL_HEADER;
  config.DISABLE_RULES_HEADER = 'x-whistle-proxy-rules-' + uid;
  config.PROXY_ID_HEADER = 'x-whistle-proxy-id-' + uid;
  config.WEBUI_HEAD = 'x-forwarded-from-whistle-' + uid;
  config.RES_RULES_HEAD = 'x-whistle-res-rules-' + uid;
  config.COMPOSER_CLIENT_ID_HEADER = 'x-whistle-client-id-' + uid;
  config.FROM_COM_HEADER = 'x-whistle-composer-' + uid;
  config.PLUGIN_HOOKS = {
    SNI: 'sni-' + uid,
    AUTH: 'auth-' + uid,
    HTTP: 'http-' + uid,
    UI: 'ui-' + uid,
    TUNNEL: 'tunnel-' + uid,
    TUNNEL_RULES: 'tunnel-rules-' + uid,
    REQ_STATS: 'req-stats-' + uid,
    RES_STATS: 'res-stats-' + uid,
    REQ_RULES: 'req-rules-' + uid,
    RES_RULES: 'res-rules-' + uid,
    REQ_READ: 'req-read-' + uid,
    REQ_WRITE: 'req-write-' + uid,
    RES_READ: 'res-read-' + uid,
    RES_WRITE: 'res-write-' + uid,
    WS_REQ_READ: 'ws-req-read-' + uid,
    WS_REQ_WRITE: 'ws-req-write-' + uid,
    WS_RES_READ: 'ws-res-read-' + uid,
    WS_RES_WRITE: 'ws-res-write-' + uid,
    TUNNEL_REQ_READ: 'tunnel-req-read-' + uid,
    TUNNEL_REQ_WRITE: 'tunnel-req-write-' + uid,
    TUNNEL_RES_READ: 'tunnel-res-read-' + uid,
    TUNNEL_RES_WRITE: 'tunnel-res-write-' + uid
  };
  
  var KEEP_ALIVE_MSECS = 10000;
  var CONN_TIMEOUT = common.CONN_TIMEOUT;
  var DNS_ORDERS_OPTS = ['', 'verbatim', 'ipv4first', hasIPv6First ? 'ipv6first' : ''];
  var DOMAIN_STAR_RE = /([*~]+)(\\.)?/g;
  
  config.CONN_TIMEOUT = CONN_TIMEOUT;
  
  var getWhistlePath = common.getWhistlePath;
  var getHostPort = common.getHostPort;
  
  var ALL_WHISTLES_PATH = path.join(getWhistlePath(), 'all_whistles');
  
  
  function domainToRegExp(_, star, dot) {
    var len = star.length;
    var result = len > 1 ? '([^/?]*)' : '([^/?.]*)';
    if (dot) {
      result += '\\.';
      if (len > 2) {
        result = '(?:' + result + ')?';
      }
    }
    return result;
  }
  
  function getDataDir(dirname) {
    var dir = path.join(getWhistlePath(), dirname || '.' + config.name);
    fse.ensureDirSync(dir);
    return dir;
  }
  
  config.baseDir = getDataDir();
  config.TEMP_FILES_PATH = path.join(getWhistlePath(), 'temp_files');
  config.CUSTOM_PLUGIN_PATH = path.join(getWhistlePath(), 'custom_plugins');
  config.CUSTOM_CERTS_DIR = path.resolve(getWhistlePath(), 'custom_certs');
  config.SAVED_SESSIONS_PATH = path.resolve(getWhistlePath(), 'saved_sessions');
  config.DEV_PLUGINS_PATH = path.resolve(getWhistlePath(), 'dev_plugins');
  config.rulesDir = path.join(config.baseDir, 'rules');
  config.valuesDir = path.join(config.baseDir, 'values');
  config.propertiesDir = path.join(config.baseDir, 'properties');
  
  try {
    fse.ensureDirSync(config.TEMP_FILES_PATH);
    var blankFile = path.join(config.TEMP_FILES_PATH, 'blank');
    if (!fs.existsSync(blankFile)) {
      fs.writeFileSync(blankFile, '');
    }
  } catch (e) {}
  try {
    fse.ensureDirSync(config.CUSTOM_CERTS_DIR);
  } catch (e) {}
  try {
    fse.ensureDirSync(config.SAVED_SESSIONS_PATH);
  } catch (e) {}
  
  exports.getWhistlePath = getWhistlePath;
  exports.getDataDir = getDataDir;
  var async_id_symbol;
  if (version[0] < 16) {
    try {
      async_id_symbol = process.binding('async_wrap').async_id_symbol;
    } catch (e) {}
  }
  var emptyHandle = {
    asyncReset: noop,
    getAsyncId: noop
  };
  
  function createAgent(agentConfig, https) {
    var agent = new (https ? httpsAgent : httpAgent)(agentConfig);
    if (async_id_symbol) {
      var addRequest = agent.addRequest;
      agent.addRequest = function (req, options) {
        // fix: https://github.com/nodejs/node/issues/13539
        var freeSockets = this.freeSockets[this.getName(options)];
        if (freeSockets && freeSockets.length) {
          var socket = freeSockets[0];
          var handle = socket._handle;
          if (!handle) {
            socket._handle = emptyHandle;
          } else if (typeof handle.asyncReset !== 'function') {
            handle.asyncReset = noop;
          }
          var originalRef = socket.ref;
          socket.ref = function () {
            socket.ref = originalRef;
            if (socket._handle === emptyHandle) {
              delete socket._handle;
            } else if (socket._handle.asyncReset === noop) {
              delete socket._handle.asyncReset;
            }
            socket.ref();
          };
        }
        var onSocket = req.onSocket;
        req.onSocket = function (socket) {
          try {
            socket[async_id_symbol] = socket._handle.getAsyncId();
          } catch (e) {}
          onSocket.apply(this, arguments);
        };
        addRequest.apply(this, arguments);
      };
    }
    return agent;
  }
  
  function capitalize(str) {
    return (str && str[0].toUpperCase()) + str.substring(1);
  }
  
  function getHttpsAgent(options, reqOpts) {
    var key = getCacheKey(options);
    var agent = httpsAgents.get(key);
    if (reqOpts) {
      var headers = options.headers || {};
      var proxyHeaders = {};
      Object.keys(headers).forEach(function (key) {
        var rawKey = key.split('-').map(capitalize).join('-');
        proxyHeaders[rawKey] = headers[key];
      });
      reqOpts._tunnelProxyHeaders = proxyHeaders;
    }
    if (!agent) {
      options.proxyAuth = options.auth;
      options.rejectUnauthorized = config.rejectUnauthorized;
      options.proxy = {
        host: options.proxyHost,
        port: options.proxyPort,
        enableIntercept: options.enableIntercept,
        keepStreamResume: options.keepStreamResume,
        proxyTunnelPath: options.proxyTunnelPath
      };
      var agentName = options.isHttps ? 'httpsOverHttp' : 'httpOverHttp';
      if (options.proxyServername) {
        options.proxy.servername = options.proxyServername;
        agentName += 's';
      }
      agent = new tunnel[agentName](options);
      httpsAgents.set(key, agent);
      agent.on('free', preventThrowOutError);
    }
    return agent;
  }
  
  exports.getHttpsAgent = getHttpsAgent;
  
  function getCacheKey(options, isSocks) {
    var auth = options.auth || options.proxyAuth;
    if (!auth) {
      var headers = options.headers || '';
      auth = headers['proxy-authorization'] || '';
    }
    var ip = (!isSocks && options.clientIp) || '';
    return [
      options.proxyServername ? 'https-proxy' : '',
      options.isHttps ? 'https' : 'http',
      options.proxyHost,
      options.proxyPort,
      auth,
      ip,
      options.proxyTunnelPath || '',
      (options.isHttps && options.cacheKey) || ''
    ].join(':');
  }
  
  function getAuths(_url) {
    var options = typeof _url == 'string' ? parseUrl(_url) : _url;
    if (!options || !options.auth) {
      return [socks.auth.None()];
    }
  
    var auths = [];
    options.auth.split('|').forEach(function (auth) {
      auth = auth.trim();
      if (auth) {
        var index = auth.indexOf(':');
        auths.push({
          username: index == -1 ? auth : auth.substring(0, index),
          password: index == -1 ? '' : auth.substring(index + 1)
        });
      }
    });
  
    return auths.length
      ? auths.map(function (auth) {
        return socks.auth.UserPassword(auth.username, auth.password);
      })
      : [socks.auth.None()];
  }
  
  exports.getAuths = getAuths;
  
  function setDefaultResultOrder(order) {
    if (!hasDnsOrder) {
      return;
    }
    order = DNS_ORDERS_OPTS.indexOf(order) === -1 ? DNS_ORDERS_OPTS[order] : order;
    if (!order) {
      return;
    }
    config.ipv4First = false;
    config.ipv6First = false;
    if (order[3] === '6') {
      config.ipv6First = true;
    } else if (order[3] === '4') {
      config.ipv4First = true;
    }
    try {
      if (dns.getDefaultResultOrder() !== order) {
        dns.setDefaultResultOrder(order);
      }
      config.dnsOrder = DNS_ORDERS_OPTS.indexOf(order);
      return true;
    } catch (e) {}
  }
  
  exports.setDefaultResultOrder = setDefaultResultOrder;
  
  exports.setAuth = function (auth) {
    if (auth) {
      config.username = getString(auth.username);
      var password = (config.password = getString(auth.password));
      config.passwordHash = password && common.createHash(password);
      setGuestAuth(auth);
    }
    return config;
  };
  
  exports.getPluginData = function (name) {
    var pluginsDataMap = name && config.pluginsDataMap;
    if (!pluginsDataMap) {
      return;
    }
    name = name.substring(name.lastIndexOf('.') + 1);
    return pluginsDataMap[name];
  };
  
  exports.setGuestAuth = function (auth) {
    auth && setGuestAuth(auth);
  };
  
  function parseHost(host) {
    if (Array.isArray(host)) {
      return host.filter(function (h) {
        return h && typeof h === 'string';
      });
    }
    host = typeof host === 'string' ? host.trim() : '';
    return host && host.split('|').map(getHostname);
  }
  
  exports.setUIHost = function (host) {
    customUIHost = parseHost(host);
  };
  
  exports.setPluginUIHost = function (pluginName, host) {
    if (!pluginName || !WHISTLE_PLUGIN_RE.test(pluginName)) {
      return;
    }
    pluginName = RegExp.$1;
    host = parseHost(host);
    if (host) {
      customPluginNameHost[pluginName] = host;
      host.forEach(function (h) {
        delete customHostPluginMap[h];
      });
    } else {
      delete customPluginNameHost[pluginName];
    }
    customHostPluginMap = {};
    Object.keys(customPluginNameHost).forEach(function (name) {
      var list = customPluginNameHost[name];
      list &&
        list.forEach(function (h) {
          customHostPluginMap[h] = name;
        });
    });
  };
  
  exports.connect = function (options, cb) {
    return common.connectInner(options, cb, config);
  };
  
  function preventThrowOutError(socket) {
    if (socket.listeners('error').indexOf(freeSocketErrorListener) === -1) {
      socket.once('error', freeSocketErrorListener);
    }
  }
  
  function freeSocketErrorListener() {
    var socket = this;
    socket.destroy();
    socket.emit('agentRemove');
    socket.removeListener('error', freeSocketErrorListener);
  }
  
  function resolvePath(file) {
    if (!file || !(file = file.trim())) {
      return file;
    }
  
    return /^[\w-]+$/.test(file) ? file : path.resolve(file);
  }
  
  function getHostname(_url) {
    if (typeof _url != 'string') {
      return '';
    }
    if (_url.indexOf('/') != -1) {
      return parseUrl(_url).hostname;
    }
    var index = _url.indexOf(':');
    return index == -1 ? _url : _url.substring(0, index);
  }
  
  function getPaths(paths, isCustom) {
    if (typeof paths === 'string') {
      paths = paths.trim().split(/\s*[|,;]\s*/);
    } else if (!Array.isArray(paths)) {
      return;
    }
    var result = [];
    paths.forEach(function (path) {
      if (isCustom && ['self', 'buildin', 'buildIn', 'build-in'].indexOf(path) !== -1) {
        result.push(config.CUSTOM_PLUGIN_PATH);
      } else if (path && typeof path === 'string') {
        result.push(path);
      }
    });
    return result.length ? result : null;
  }
  
  function getSecureFilter(newConf) {
    var secureFilter = newConf.secureFilter;
    if (typeof secureFilter === 'function') {
      return secureFilter;
    }
    if (!secureFilter || typeof secureFilter !== 'string') {
      return;
    }
    try {
      return require(secureFilter);
    } catch (e) {}
  }
  
  function getString(str) {
    return str && typeof str === 'string' ? str : '';
  }
  
  function readFileText(filepath) {
    var text = common.readFileTextSync(filepath);
    return text && text.trim();
  }
  
  function readFileBuffer(filepath, required) {
    return common.readFileBufferSync(filepath, required === false);
  }
  
  function isPort(port) {
    return /^\d+$/.test(port) && port <= 65535 && port > 0;
  }
  
  function setGuestAuth(auth) {
    if (auth.guestName === '-' && !auth.guestPassword) {
      config.guest = {};
    } else if (auth.guestName && auth.guestPassword) {
      config.guest = {
        username: auth.guestName,
        password: auth.guestPassword
      };
    } else if ('guest' in auth) {
      config.guest = auth.guest;
    }
  }
  
  function parseString(str) {
    str = str && typeof str === 'string' && str.trim();
    if (!str) {
      return '';
    }
    str = readFileText(str) || str;
    if (!/\s/.test(str) && str.indexOf('%') !== -1) {
      try {
        str = decodeURIComponent(str);
      } catch (e) {}
    }
    if (/^\{[\s\S]+\}$/.test(str) || /^\[[\s\S]+\]$/.test(str)) {
      try {
        return json5.parse(str);
      } catch (e) {}
    }
    return str;
  }
  
  function getPluginList(list) {
    if (typeof list == 'string') {
      list = list.trim().split(/\s*[,|]\s*/);
    }
    var result;
    if (Array.isArray(list)) {
      list.forEach(function (name) {
        if (WHISTLE_PLUGIN_RE.test(name)) {
          result = result || [];
          result.push(RegExp.$1);
        }
      });
    }
    return result;
  }
  
  function readUIExt(uiExt) {
    if (!uiExt) {
      return;
    }
    var htmlPrepend = uiExt.htmlPrepend && readFileBuffer(uiExt.htmlPrepend, uiExt.required);
    var htmlAppend = uiExt.htmlAppend && readFileBuffer(uiExt.htmlAppend, uiExt.required);
    var jsPrepend = uiExt.jsPrepend && readFileBuffer(uiExt.jsPrepend, uiExt.required);
    var jsAppend = uiExt.jsAppend && readFileBuffer(uiExt.jsAppend, uiExt.required);
    if (htmlPrepend ||htmlAppend || jsPrepend || jsAppend) {
      if (htmlPrepend) {
        htmlPrepend = Buffer.concat([Buffer.from('<!DOCTYPE html>\n'), htmlPrepend]);
      }
      return {
        htmlPrepend: htmlPrepend,
        htmlAppend: htmlAppend,
        jsPrepend: jsPrepend,
        jsAppend: jsAppend
      };
    }
  }
  
  exports.extend = function (newConf) {
    config.pluginHostMap = {};
    config.uiport = config.port;
    config.ipv6Only = false;
    var rcConf = common.readWhistleRc(newConf);
    newConf = extend({}, rcConf, newConf);
    if (rcConf) {
      if (rcConf.clearPreOptions) {
        newConf.clearPreOptions = true;
      }
      if (rcConf.noGlobalPlugins) {
        newConf.noGlobalPlugins = true;
      }
    }
    if (process.env.WHISTLE_MODE) {
      newConf.mode = process.env.WHISTLE_MODE + '|' + (newConf.mode || '');
    }
    var useAgent;
    var newAgentConf;
  
    if (newConf.specialPath && typeof newConf.specialPath === 'string') {
      config.SPEC_PATH = newConf.specialPath;
    }
    if (newConf.whistleName && common.isWhistleName(newConf.whistleName)) {
      config.whistleName = newConf.whistleName;
    }
    config.uiExt = readUIExt(newConf.uiExt);
    config.specialAuth = newConf.specialAuth;
    config.uiMiddleware = newConf.uiMiddlewares || newConf.uiMiddleware;
    newAgentConf = newConf.agentConfig;
    if (newConf.cmdName && CMD_RE.test(newConf.cmdName)) {
      config.cmdName = newConf.cmdName;
    }
    if (newConf.account && typeof newConf.account === 'string') {
      config.account = newConf.account;
    }
    config.allowPluginList = getPluginList(newConf.allowPluginList);
    config.blockPluginList = getPluginList(newConf.blockPluginList);
  
    if (newConf.webUIPath && /^[\w.-]+$/.test(newConf.webUIPath)) {
      config.WEBUI_PATH = '/.' + newConf.webUIPath + config.WEBUI_PATH.substring(1);
    }
    if (newConf.cluster) {
      config.headless = true;
      config.workerIndex = process.env.workerIndex;
      if (typeof newConf.mode !== 'string') {
        newConf.mode = '';
      }
    }
    var dnsServer = newConf.dnsServer;
    var resolve6;
    var dnsOptional;
    if (common.isString(dnsServer)) {
      dnsServer = dnsServer.trim();
      if (common.isUrl(dnsServer)) {
        config.dnsOverHttps = dnsServer;
      } else {
        dnsServer = dnsServer.split(/[|,&]/);
      }
    }
  
    var allowOrigin = newConf.allowOrigin;
    if (common.isString(allowOrigin)) {
      allowOrigin = allowOrigin.trim().toLowerCase().split(/\s*[|,&]\s*/);
      if (allowOrigin.indexOf('*') === -1) {
        allowOrigin = allowOrigin.map(function(h) {
          if (h.indexOf('*') === -1) {
            return h;
          }
          h = common.escapeRegExp(h);
          h = h.replace(DOMAIN_STAR_RE, domainToRegExp);
          try {
            return new RegExp('^' + h + '$');
          } catch (e) {}
        }).filter(function(h) {
          return h;
        });
        if (allowOrigin.length) {
          config.allowOrigin = allowOrigin;
        }
      } else {
        config.allowAllOrigin = true;
      }
    }
  
    if (dns.getServers && Array.isArray(dnsServer)) {
      var newServers = [];
      dnsServer.forEach(function (ip) {
        ip = typeof ip === 'string' && ip.trim();
        if (/^ipv6$/i.test(ip)) {
          resolve6 = true;
        } else if (ip === 'optional' || ip === 'default' || ip === 'fallback') {
          dnsOptional = true;
        } else if (net.isIP(ip)) {
          newServers.push(ip);
        } else if (IPV4_RE.test(ip) || IPV6_RE.test(ip)) {
          var dnsIp = RegExp.$1;
          var dnsPort = RegExp.$2;
          if (net.isIP(dnsIp) && (!dnsPort || isPort(dnsPort.substring(1)))) {
            newServers.push(ip);
          }
        }
      });
      if (newServers.length) {
        dns.setServers(newServers);
        config.resolve6 = resolve6;
        config.dnsOptional = dnsOptional;
        config.dnsServer = newServers.join();
      }
    }
  
    if (newConf.inspect || newConf.inspectBrk) {
      config.inspectMode = true;
      process.env.PFORK_MODE = 'inline';
    }
    variableProperties.forEach(function (name) {
      config[name] = newConf[name] || pkgConf[name];
    });
    var shadowRules = parseString(newConf.shadowRules);
    var resolveShadowValues = function (obj) {
      if (
          (!obj.rules || typeof obj.rules === 'string') &&
          obj.values &&
          typeof obj.values !== 'string'
        ) {
        config.shadowRules = obj.rules || obj.rules.trim();
        if (Object.keys(obj.values).length) {
          config.shadowValues = obj.values;
        }
      } else {
        config.rules = extend({}, newConf.rules, obj);
      }
    };
    if (typeof shadowRules === 'string') {
      config.shadowRules = shadowRules.trim();
    } else if (Array.isArray(shadowRules)) {
      if (typeof shadowRules[0] === 'string') {
        config.shadowRules = shadowRules[0].trim();
        if (shadowRules[1]) {
          config.rules = extend({}, newConf.rules, shadowRules[1]);
        }
        if (shadowRules[2]) {
          config.values = extend({}, newConf.values, shadowRules[2]);
        }
      } else if (shadowRules[0]) {
        resolveShadowValues(shadowRules[0]);
        if (shadowRules[1]) {
          config.values = extend({}, newConf.values, shadowRules[1]);
        }
      } else if (shadowRules[1]) {
        config.values = extend({}, newConf.values, shadowRules[1]);
      }
    } else if (shadowRules) {
      resolveShadowValues(shadowRules);
    }
    var extra = newConf.extra;
    if (common.isString(extra)) {
      extra = /^\s*\{[\w\W]*\}\s*$/.test(extra) ? extra : readFileText(extra);
      try {
        extra = extra && JSON.parse(extra);
        if (extra && typeof extra === 'object') {
          config.pluginsDataMap = extend({}, config.pluginsDataMap, extra);
        }
      } catch (e) {}
      extra = null;
    }
    var customHandler = newConf.customHandler || newConf.customHandle;
    if (typeof customHandler === 'function') {
      config.customHandler = customHandler;
    }
    if (isPort(newConf.realPort) && config.realPort != config.port) {
      config.realPort = newConf.realPort;
    }
    var realHost = newConf.realHost;
    if (typeof realHost === 'string' && (!realHost || /^[\w.-]+$/.test(newConf.realHost))) {
      config.realHost = newConf.realHost;
    }
  
    var socksPort = getHostPort(newConf.socksPort);
    if (socksPort) {
      config.socksPort = socksPort.port;
      config.socksHost = socksPort.host;
    }
    var httpPort = getHostPort(newConf.httpPort);
    if (httpPort) {
      config.httpPort = httpPort.port;
      config.httpHost = httpPort.host;
    }
    var httpsPort = getHostPort(newConf.httpsPort);
    if (httpsPort) {
      config.httpsPort = httpsPort.port;
      config.httpsHost = httpsPort.host;
    }
  
    if (common.isString(newConf.host)) {
      config.defaultHost = newConf.host;
    }
  
    if (typeof newConf.authKey === 'string') {
      config.authKey = newConf.authKey;
    }
    if (typeof newConf.guestAuthKey === 'string') {
      config.guestAuthKey = newConf.guestAuthKey;
    }
    if (newConf.reqCacheSize > 0) {
      config.reqCacheSize = newConf.reqCacheSize;
    }
    if (newConf.frameCacheSize > 0) {
      config.frameCacheSize = newConf.frameCacheSize;
    }
    config.allowMultipleChoice = newConf.allowMultipleChoice;
    if (common.isString(newConf.mode)) {
      var mode = newConf.mode.trim().split(/\s*[|,&]\s*/);
      var useResolve;
      var dnsFallback;
      if (mode.indexOf('admin') !== -1) {
        var admin = 'proxyServer|master|x-forwarded-proto';
        admin = config.debugMode ? admin : `${admin}|strict|rules|disableUpdateTips|proxifier|notAllowedDisablePlugins`;
        mode = admin.split('|').concat(mode);
      }
      if (mode.indexOf('multiple') !== -1) {
        mode = 'multiEnv|disableUpdateTips|keepXFF|x-forwarded-proto'.split('|').concat(mode);
      }
      mode.forEach(function (m) {
        m = m.trim();
        if (
            /^(pureProxy|debug|ipv6Only|master|client|diagnose|disableAuthUI|captureData|headless|strict|proxyServer|encrypted|noGzip|disableUpdateTips|proxifier2?)$/.test(m)
          ) {
          config[m] = true;
        } else if(m === 'ipv6only') {
          config.ipv6Only = true;
        } else if (m === 'keepProxyUI') {
          config.keepProxyUI = true;
        } else if (m === 'agent') {
          useAgent = true;
        } else if (m === 'disableUIAuth') {
          config.disableAuthUI = true;
        } else if (m === 'showPluginReq') {
          config.showPluginReq = config.debugMode;
        } else if (m === 'disableCustomCerts') {
          config.disableCustomCerts = true;
        } else if (m === 'nohost' || m === 'multienv' || m === 'multiEnv') {
          config[m] = true;
          config.multiEnv = true;
        } else if (DNS_ORDERS.indexOf(m) !== -1) {
          setDefaultResultOrder(m.toLowerCase());
        } else if (m === 'proxyOnly') {
          config.pureProxy = true;
        } else if (m === 'useMultipleRules' || m === 'enableMultipleRules') {
          config.allowMultipleChoice = true;
        } else if (m === 'disableMultipleRules') {
          config.allowMultipleChoice = false;
        } else if (
          m === 'notAllowDisableRules' ||
          m === 'notAllowedDisableRules'
        ) {
          config.notAllowedDisableRules = true;
        } else if (m === 'disableBackOption' || m === 'disabledBackOption') {
          config.disabledBackOption = true;
        } else if (
          m === 'disableMultipleOption' ||
          m === 'disabledMultipleOption'
        ) {
          config.disabledMultipleOption = true;
        } else if (
          m === 'disableRulesOptions' ||
          m === 'disabledRulesOptions'
        ) {
          config.disabledRulesOptions = true;
          config.disabledBackOption = true;
          config.disabledMultipleOption = true;
          config.notAllowedDisableRules = true;
        } else if (
          m === 'notAllowDisablePlugins' ||
          m === 'notAllowedDisablePlugins'
        ) {
          config.notAllowedDisablePlugins = true;
        } else if (
          m === 'notAllowEnableHTTPS' ||
          m === 'notAllowedEnableHTTPS'
        ) {
          config.notAllowedEnableHTTPS = true;
        } else if (m === 'socks') {
          config.socksMode = true;
        } else if (m === 'network') {
          config.networkMode = true;
          config.captureData = true;
        } else if (m === 'shadowRules') {
          config.shadowRulesMode = true;
        } else if (m === 'shadowRulesOnly') {
          config.shadowRulesMode = true;
          config.disableWebUI = true;
        } else if (m === 'plugins') {
          config.pluginsMode = true;
        } else if (m === 'pluginsOnly') {
          config.pluginsOnlyMode = true;
        } else if (m === 'rules') {
          config.rulesMode = true;
        } else if (m === 'rulesOnly') {
          config.rulesOnlyMode = true;
        } else if (m === 'httpProxy') {
          config.pureProxy = true;
        } else if (
          m === 'keepXFF' ||
          m === common.CLIENT_IP_HEADER ||
          m === 'forwardedFor'
        ) {
          config.keepXFF = true;
        } else if (m === 'x-forwarded-host') {
          config.enableFwdHost = true;
        } else if (m === 'x-forwarded-proto') {
          config.enableFwdProto = true;
        } else if (m === 'INADDR_ANY') {
          config.INADDR_ANY = true;
        } else if (m === 'buildIn' || m === 'build-in') {
          process.env.PFORK_EXEC_PATH = process.execPath;
        } else if (m === 'safe' || m === 'rejectUnauthorized') {
          config.rejectUnauthorized = true;
        } else if (m === 'enableRequestHeaderRules') {
          config.enableRequestHeaderRules = true;
        } else if (m === 'persistentCapture') {
          config.isEnableCapture = true;
          config.persistentCapture = true;
        } else if (['capture', 'intercept', 'enable-capture', 'enableHttps', 'enableHTTPS', 'enableCapture'].indexOf(m) !== -1) {
          config.isEnableCapture = true;
        } else if (['disable-capture', 'disableCapture'].indexOf(m) !== -1) {
          config.isEnableCapture = false;
        } else if (['http2',  'enable-http2', 'enableHttp2', 'enable-h2', 'enableH2'].indexOf(m) !== -1) {
          config.isEnableHttp2 = true;
        } else if (
          ['disable-http2', 'disableHttp2', 'disable-h2', 'disableH2'].indexOf(m) !== -1
        ) {
          config.isEnableHttp2 = false;
        } else if (m === 'hideLeftBar' || m === 'hideLeftMenu') {
          config.hideLeftMenu = true;
        } else if (DATA_KEY_RE.test(m)) {
          var type = RegExp.$1;
          var override = RegExp.$2 === '=';
          var key = RegExp.$3.toLowerCase();
          switch (type) {
          case 'clientip':
            config.overCipKey = override;
            config.cipKey = key;
            break;
          case 'clientid':
            config.overCidKey = override;
            config.cidKey = key;
            break;
          default:
            config.overTdKey = override;
            config.tdKey = key;
            break;
          }
        } else if (['dnsResolve', 'dnsResolve4', 'dnsResolve6'].indexOf(m) !== -1) {
          useResolve = true;
          config[m] = true;
        } else if (m === 'dnsFallback') {
          dnsFallback = true;
        }
      });
  
      if (useResolve && dnsFallback) {
        config.dnsFallback = true;
      }
      if (config.headless) {
        if (!config.pluginsMode) {
          config.noGlobalPlugins = true;
        }
        config.pluginsOnlyMode = true;
        config.disableWebUI = true;
        delete config.rulesOnlyMode;
      } else if (config.shadowRulesMode) {
        config.networkMode = true;
        delete config.rulesOnlyMode;
        delete config.pluginsOnlyMode;
      }
      if (config.rulesOnlyMode) {
        delete config.pluginsOnlyMode;
        delete config.networkMode;
        delete config.pluginsMode;
        config.rulesMode = true;
      } else if (config.pluginsOnlyMode) {
        delete config.networkMode;
        config.rulesMode = true;
        config.pluginsMode = true;
      } else if (config.networkMode) {
        delete config.rulesMode;
        delete config.pluginsMode;
      } else if (config.rulesMode) {
        delete config.pluginsMode;
        delete config.networkMode;
      }
    }
    if (/^\d+$/.test(newConf.timeout)) {
      config.timeout = +newConf.timeout;
    }
  
    setGuestAuth(newConf);
    config.disableAllRules = newConf.disableAllRules;
    config.disableAllPlugins = newConf.disableAllPlugins;
    if (newConf.replaceExistRule === false) {
      config.replaceExistRule = false;
    } else {
      config.replaceExistRule = newConf.replaceRules;
    }
    if (newConf.replaceExistValue === false) {
      config.replaceExistValue = false;
    } else {
      config.replaceExistValue = newConf.replaceValues;
    }
    if (common.isString(newConf.certDir)) {
      config.certDir = path.resolve(newConf.certDir);
    }
    var mw = newConf.middlewares || newConf.middleware;
    if (typeof mw == 'string') {
      config.middlewares = mw.trim().split(/\s*,\s*/);
    }
    var secureFilter = getSecureFilter(newConf);
    if (typeof secureFilter === 'function') {
      config.secureFilter = secureFilter;
    }
    if (typeof newConf.installPlugins === 'function') {
      config.installPlugins = newConf.installPlugins;
      config.hasInstaller = true;
    }
    if (typeof newConf.handleWebReq === 'function') {
      config.handleWebReq = newConf.handleWebReq;
    }
    if (typeof newConf.handleUpdate === 'function') {
      config.handleUpdate = newConf.handleUpdate;
      config.hasUpdater = true;
    }
    config.notUninstallPluginPaths = getPaths(newConf.notUninstallPluginPath || newConf.notUninstallPluginPaths);
    config.pluginPaths = getPaths(newConf.pluginPaths || newConf.pluginsPath || newConf.pluginPath);
    config.prePluginsPath = config.projectPluginPaths = getPaths(newConf.projectPluginPaths || newConf.projectPluginsPath || newConf.projectPluginPath);
    config.accountPluginsPath = getPaths(newConf.accountPluginPaths || newConf.accountPluginsPath || newConf.accountPluginPath);
    config.customPluginPaths = getPaths(newConf.customPluginPaths || newConf.customPluginsPath || newConf.customPluginPath, true);
    config.addon = getPaths(newConf.addonsPath || newConf.addonPath || newConf.addon);
    if (config.accountPluginsPath) {
      config.customPluginPaths = config.accountPluginsPath.concat(config.customPluginPaths || []);
    }
    if (config.customPluginPaths) {
      config.prePluginsPath = (config.prePluginsPath || []).concat(config.customPluginPaths);
    }
  
    var pluginHost = newConf.pluginHost;
    if (typeof pluginHost === 'string' && (pluginHost = pluginHost.trim())) {
      pluginHost = qs.parse(pluginHost);
      Object.keys(pluginHost).forEach(function (name) {
        var host = pluginHost[name];
        if (typeof host === 'string' && (host = host.trim())) {
          host = host.toLowerCase().split(/\s*[|,&]\s*/);
          host.forEach(function (h) {
            config.pluginHostMap[h] = name;
          });
        }
      });
    }
  
    var port = getHostPort(newConf.port);
    if (port) {
      config.host = port.host;
      config.uiport = config.port = port.port;
    }
    if (config.disableWebUI) {
      config.notAllowedDisablePlugins = true;
    } else {
      var uiPort = getHostPort(newConf.uiport);
      if (uiPort) {
        var curHost = config.host || config.defaultHost;
        if (curHost && uiPort.host && curHost !== uiPort.host) {
          config.customUIPort = true;
        } else {
          config.customUIPort = uiPort.port != config.port;
        }
        config.uiport = uiPort.port;
        config.uihost = uiPort.host;
      }
    }
  
  
    if (!config.rulesMode) {
      config.captureData = true;
    }
  
    if (!config.authKey) {
      config.authKey = 'auto_' + (Date.now() + Math.random().toFixed(6));
    }
    config.middlewares = Array.isArray(config.middlewares)
      ? config.middlewares.map(resolvePath)
      : [];
    var uiHostList = (config.uiHostList = []);
    if (typeof config.localUIHost === 'string') {
      var localHostList = config.localUIHost.toLowerCase().split(/\s*[|,&]\s*/);
      localHostList.forEach(function (hostname) {
        hostname = getHostname(hostname);
        if (hostname && LOCAL_UI_HOST_LIST.indexOf(hostname) == -1) {
          uiHostList.push(hostname);
        }
      });
      LOCAL_UI_HOST_LIST.push.apply(LOCAL_UI_HOST_LIST, uiHostList);
      // 兼容老版本
      config.customLocalUIHost = uiHostList[0];
    }
  
    config.setLocalUIHost = function (hostname) {
      config.customLocalUIHost = getHostname(hostname);
    };
  
    config.localUIHost = 'local.whistlejs.com';
  
    var isWebUIHost = function (host) {
      if (
        host === 'local.wproxy.org' ||
        uiHostList.indexOf(host) !== -1 ||
        (customUIHost && customUIHost.indexOf(host) !== -1)
      ) {
        return true;
      }
      if (config.pureProxy) {
        return false;
      }
      return LOCAL_UI_HOST_LIST.indexOf(host) !== -1;
    };
    var isLocalUIUrl = function (url) {
      var host = getHostname(url);
      return (
        isWebUIHost(host) ||
        !!config.pluginHostMap[host] ||
        !!customHostPluginMap[host]
      );
    };
    config.isLocalUIUrl = isLocalUIUrl;
    exports.isWebUIHost = isWebUIHost;
    exports.getPluginNameByHost = function (host) {
      host = getHostname(host);
      if (isWebUIHost(host)) {
        return;
      }
      return config.pluginHostMap[host] || customHostPluginMap[host];
    };
  
    config.sockets = Math.max(parseInt(config.sockets, 10) || 0, 1);
    if (config.strict) {
      config.sockets = Math.max(config.sockets, 360);
    }
    var agentConfig = newAgentConf || {
      maxSockets: config.sockets,
      keepAlive: true,
      keepAliveMsecs: KEEP_ALIVE_MSECS,
      maxFreeSockets: 0
    };
    // node 11及以上版本缓存连接有问题，先禁掉
    disableAgent = !newAgentConf && !useAgent && (disableAgent || config.debug);
    config.httpAgent = disableAgent ? false : createAgent(agentConfig);
    config.httpsAgent = disableAgent ? false : createAgent(agentConfig, true);
    config.getSocksAgent = function (options) {
      var key = getCacheKey(options, true);
      var agent = socksAgents.get(key);
      if (agent) {
        return agent;
      }
      extend(options, agentConfig);
      options.proxyPort = parseInt(options.proxyPort, 10) || 1080;
      options.rejectUnauthorized = config.rejectUnauthorized;
      options.localDNS = false;
      options.auths = getAuths(options);
      agent = options.isHttps
        ? new socks.HttpsAgent(options)
        : new socks.HttpAgent(options);
      socksAgents.set(key, agent);
      agent.on('free', preventThrowOutError);
      return agent;
    };
    var baseDir = config.baseDir
      ? path.resolve(common.getHomePath(config.baseDir), config.dataDirname)
      : (config.whistleName ? path.join(ALL_WHISTLES_PATH, config.whistleName) : getDataDir(config.dataDirname));
    var customDirs = path.join(baseDir, 'custom_dirs');
    config.baseDir = baseDir;
    config.storage = config.storage && encodeURIComponent(config.storage);
    if (config.storage) {
      baseDir = path.join(customDirs, config.storage);
    }
    if (config.whistleName) {
      config.CUSTOM_PLUGIN_PATH = path.join(baseDir, 'custom_plugins');
      config.CUSTOM_CERTS_DIR = path.resolve(baseDir, 'custom_certs');
      config.SAVED_SESSIONS_PATH = path.resolve(baseDir, 'saved_sessions');
      fse.ensureDirSync(config.CUSTOM_PLUGIN_PATH);
      fse.ensureDirSync(config.CUSTOM_CERTS_DIR);
      fse.ensureDirSync(config.SAVED_SESSIONS_PATH);
    }
    fse.ensureDirSync(baseDir);
    config.baseDirHash = common.createHash(baseDir);
    if (config.password) {
      config.passwordHash = common.createHash(config.password);
    }
    config.rulesDir = path.join(baseDir, 'rules');
    config.valuesDir = path.join(baseDir, 'values');
    config.propertiesDir = path.join(baseDir, 'properties');
    if (config.storage && newConf.copy) {
      var copyDir =
        typeof newConf.copy == 'string' && encodeURIComponent(newConf.copy);
      if (copyDir !== config.storage) {
        var dataDir = copyDir ? path.join(customDirs, copyDir) : config.baseDir;
        var rulesDir = path.join(dataDir, 'rules');
        var valuesDir = path.join(dataDir, 'values');
        var propsDir = path.join(dataDir, 'properties');
        fse.ensureDirSync(rulesDir);
        fse.ensureDirSync(valuesDir);
        fse.ensureDirSync(propsDir);
        fse.copySync(rulesDir, config.rulesDir);
        fse.copySync(valuesDir, config.valuesDir);
        fse.copySync(propsDir, config.propertiesDir);
      }
    }
    var clientIdFile = path.join(config.baseDir, '.clientid');
    var clientId = readFileText(clientIdFile);
    if (!clientId) {
      clientId = [
        Date.now(),
        Math.random(),
        Math.random(),
        Math.random(),
        Math.random(),
        Math.random()
      ].join();
      fs.writeFileSync(clientIdFile, clientId);
    }
    if (config.whistleName) {
      clientId += '/' + config.whistleName;
    }
    config.clientId = clientId;
    config.clientIdNo = common.getStatSync(clientIdFile).ino + '';
    config.setModified = function (clientId, isRules) {
      if (isRules) {
        config.mrulesClientId = clientId || '';
        config.mrulesTime = Date.now();
      } else {
        config.mvaluesClientId = clientId || '';
        config.mvaluesTime = Date.now();
      }
    };
    return config;
  };
  