'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _bluebird = require('bluebird');

var _bluebird2 = _interopRequireDefault(_bluebird);

var _events = require('events');

var _events2 = _interopRequireDefault(_events);

var _url = require('url');

var _url2 = _interopRequireDefault(_url);

var _net = require('net');

var _net2 = _interopRequireDefault(_net);

var _http = require('http');

var _http2 = _interopRequireDefault(_http);

var _path = require('path');

var _path2 = _interopRequireDefault(_path);

var _querystring = require('querystring');

var _querystring2 = _interopRequireDefault(_querystring);

var _headerCaseNormalizer = require('header-case-normalizer');

var _headerCaseNormalizer2 = _interopRequireDefault(_headerCaseNormalizer);

var _request = require('request');

var _request2 = _interopRequireDefault(_request);

var _mime = require('mime');

var _mime2 = _interopRequireDefault(_mime);

var _socks5Client = require('socks5-client');

var _socks5Client2 = _interopRequireDefault(_socks5Client);

var _Agent = require('socks5-http-client/lib/Agent');

var _Agent2 = _interopRequireDefault(_Agent);

var _pacProxyAgent = require('pac-proxy-agent');

var _pacProxyAgent2 = _interopRequireDefault(_pacProxyAgent);

var _electron = require('electron');

var _config = require('./config');

var _config2 = _interopRequireDefault(_config);

var _utils = require('./utils');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

const { ROOT } = global;

const fs = require('fs-extra');
const zlib = _bluebird2.default.promisifyAll(require('zlib'));

const resolveBody = (encoding, body) => {
  function* _ref2(resolve, reject) {
    try {
      let decoded = null;
      switch (encoding) {
        case 'gzip':
          decoded = yield zlib.gunzipAsync(body);
          break;
        case 'deflate':
          decoded = yield zlib.inflateAsync(body);
          break;
        default:
          decoded = body;
      }
      decoded = decoded.toString();
      if (decoded.indexOf('svdata=') === 0) {
        decoded = decoded.substring(7);
      }
      decoded = JSON.parse(decoded);
      resolve(decoded);
    } catch (e) {
      reject(e);
    }
  }

  return new Promise((() => {
    var _ref = _asyncToGenerator(_ref2);

    return function (_x, _x2) {
      return _ref.apply(this, arguments);
    };
  })());
};
const isStaticResource = (pathname, hostname) => {
  if (pathname.startsWith('/kcs/') && pathname.indexOf('Core.swf') === -1) {
    return true;
  }
  if (pathname.startsWith('/gadget/')) {
    return true;
  }
  if (pathname.startsWith('/kcscontents/')) {
    return true;
  }
  if (hostname.match('kanpani.jp')) {
    return true;
  }
  if (hostname.match('assets.shiropro-re.net')) {
    return true;
  }
  if (hostname.match('swordlogic.com')) {
    return true;
  }
  if (hostname.match('dugrqaqinbtcq.cloudfront.net')) {
    return true;
  }
  if (hostname.match('static.touken-ranbu.jp')) {
    return true;
  }
  return false;
};
const getCachePath = pathname => {
  const dir = _config2.default.get('poi.cachePath', global.DEFAULT_CACHE_PATH);
  return _path2.default.join(dir, pathname);
};
const findHack = pathname => {
  let loc = getCachePath(_path2.default.join('KanColle', pathname));
  const sp = loc.split('.');
  const ext = sp.pop();
  sp.push('hack');
  sp.push(ext);
  loc = sp.join('.');
  try {
    fs.accessSync(loc, fs.R_OK);
    return loc;
  } catch (e) {
    if (e.code !== 'ENOENT') console.error(`error while loading hack file ${loc}`, e);
    return null;
  }
};
const findCache = (pathname, hostname) => {
  let loc;
  if (hostname.match('kanpani.jp')) {
    loc = getCachePath(_path2.default.join('Kanpani', pathname));
  } else if (hostname.match('assets.shiropro-re.net')) {
    loc = getCachePath(_path2.default.join('ShiroPro', pathname));
  } else if (hostname.match('swordlogic.com')) {
    loc = getCachePath(_path2.default.join('Shinken', pathname.replace(/^\/[0-9]{10}/, '')));
  } else if (hostname.match('dugrqaqinbtcq.cloudfront.net')) {
    loc = getCachePath(_path2.default.join('FlowerKnightGirls', pathname));
  } else if (hostname.match('static.touken-ranbu.jp')) {
    loc = getCachePath(_path2.default.join('ToukenRanbu', pathname));
  } else {
    loc = getCachePath(_path2.default.join('KanColle', pathname));
  }
  try {
    fs.accessSync(loc, fs.R_OK);
    return loc;
  } catch (error) {
    return null;
  }
};

const PacAgents = {};
const resolve = req => {
  switch (_config2.default.get('proxy.use')) {
    // HTTP Request via SOCKS5 proxy
    case 'socks5':
      return Object.assign(req, {
        agentClass: _Agent2.default,
        agentOptions: {
          socksHost: _config2.default.get('proxy.socks5.host', '127.0.0.1'),
          socksPort: _config2.default.get('proxy.socks5.port', 1080)
        }
      });
    // HTTP Request via HTTP proxy
    case 'http':
      {
        const host = _config2.default.get('proxy.http.host', '127.0.0.1');
        const port = _config2.default.get('proxy.http.port', 8118);
        const requirePassword = _config2.default.get('proxy.http.requirePassword', false);
        const username = _config2.default.get('proxy.http.username', '');
        const password = _config2.default.get('proxy.http.password', '');
        const useAuth = requirePassword && username !== '' && password !== '';
        const strAuth = `${username}:${password}@`;
        return Object.assign(req, {
          proxy: `http://${useAuth ? strAuth : ''}${host}:${port}`
        });
      }
    // PAC
    case 'pac':
      {
        const uri = _config2.default.get('proxy.pacAddr');
        if (!PacAgents[uri]) {
          PacAgents[uri] = new _pacProxyAgent2.default(uri);
        }
        return Object.assign(req, {
          agent: PacAgents[uri]
        });
      }
    // Directly
    default:
      return req;
  }
};

const isKancolleGameApi = pathname => pathname.startsWith('/kcsapi');

function _ref8(req, client, head) {
  delete req.headers['proxy-connection'];
  // Disable HTTP Keep-Alive
  req.headers['connection'] = 'close';
  const remoteUrl = _url2.default.parse(`https://${req.url}`);
  let remote = null;

  function _ref4() {
    client.write("HTTP/1.1 200 Connection Established\r\nConnection: close\r\n\r\n");
    remote.write(head);
  }

  function _ref5(data) {
    remote.write(data);
  }

  function _ref6(data) {
    client.write(data);
  }

  function _ref7() {
    client.write("HTTP/1.1 200 Connection Established\r\nConnection: close\r\n\r\n");
    remote.write(head);
    client.pipe(remote);
    remote.pipe(client);
  }

  switch (_config2.default.get('proxy.use')) {
    case 'socks5':
      {
        // Write data directly to SOCKS5 proxy
        remote = _socks5Client2.default.createConnection({
          socksHost: _config2.default.get('proxy.socks5.host', '127.0.0.1'),
          socksPort: _config2.default.get('proxy.socks5.port', 1080),
          host: remoteUrl.hostname,
          port: remoteUrl.port
        });
        remote.on('connect', _ref4);
        client.on('data', _ref5);
        remote.on('data', _ref6);
        break;
      }
    case 'http':
      {
        // Write data directly to HTTP proxy
        const host = _config2.default.get('proxy.http.host', '127.0.0.1');
        const port = _config2.default.get('proxy.http.port', 8118);
        // Write header to http proxy
        let msg = `CONNECT ${remoteUrl.hostname}:${remoteUrl.port} HTTP/${req.httpVersion}\r\n`;
        for (const k in req.headers) {
          msg += `${(0, _headerCaseNormalizer2.default)(k)}: ${req.headers[k]}\r\n`;
        }
        msg += "\r\n";
        remote = _net2.default.connect(port, host, () => {
          remote.write(msg);
          remote.write(head);
          client.pipe(remote);
          remote.pipe(client);
        });
        break;
      }
    default:
      {
        // Connect to remote directly
        remote = _net2.default.connect(remoteUrl.port, remoteUrl.hostname, _ref7);
      }
  }
  client.on('end', () => {
    remote.end();
  });
  remote.on('end', () => {
    client.end();
  });
  client.on('error', e => {
    (0, _utils.error)(e);
    remote.destroy();
  });
  remote.on('error', e => {
    (0, _utils.error)(e);
    client.destroy();
  });
  client.on('timeout', () => {
    client.destroy();
    remote.destroy();
  });
  remote.on('timeout', () => {
    client.destroy();
    remote.destroy();
  });
}

function _ref9(err) {
  (0, _utils.error)(err);
}

class Proxy extends _events2.default {
  constructor() {
    var _this;

    _this = super();

    this.load = () => {
      const serverList = fs.readJsonSync(_path2.default.join(ROOT, 'assets', 'data', 'server.json'));
      let currentServer = null;
      // HTTP Requests
      this.server = _http2.default.createServer((req, res) => {
        delete req.headers['proxy-connection'];
        // Disable HTTP Keep-Alive
        req.headers['connection'] = 'close';
        const parsed = _url2.default.parse(req.url);
        const isGameApi = parsed.pathname.startsWith('/kcsapi');
        if (isGameApi && serverList[parsed.hostname] && currentServer !== serverList[parsed.hostname].num) {
          currentServer = serverList[parsed.hostname].num;
          this.emit('network.get.server', Object.assign(serverList[parsed.hostname], {
            ip: parsed.hostname
          }));
        }
        let cacheFile = null;
        if (isStaticResource(parsed.pathname, parsed.hostname)) {
          cacheFile = findHack(parsed.pathname) || findCache(parsed.pathname, parsed.hostname);
        }
        let reqBody = Buffer.alloc(0);
        // Get all request body
        req.on('data', data => {
          reqBody = Buffer.concat([reqBody, data]);
        });
        req.on('end', _asyncToGenerator(function* () {
          let domain, pathname, requrl;
          try {
            let options = {
              method: req.method,
              url: req.url,
              headers: req.headers,
              encoding: null,
              followRedirect: false
              // Add body to request
            };if (reqBody.length > 0) {
              options = Object.assign(options, {
                body: reqBody
              });
            }
            // Use cache file

            function _ref3(promise_resolve, promise_reject) {
              (0, _request2.default)(resolve(options), function (err, res_response, res_body) {
                if (!err) {
                  promise_resolve([res_response, res_body]);
                } else {
                  promise_reject(err);
                }
              }).pipe(res);
            }

            if (cacheFile) {
              const stats = yield fs.stat(cacheFile);
              // Cache is new
              if (req.headers['if-modified-since'] && new Date(req.headers['if-modified-since']) >= stats.mtime) {
                res.writeHead(304, {
                  'Server': 'nginx',
                  'Last-Modified': stats.mtime.toGMTString()
                });
                res.end();
              } else {
                // Cache is old
                const data = yield fs.readFile(cacheFile);
                res.writeHead(200, {
                  'Server': 'nginx',
                  'Content-Length': data.length,
                  'Content-Type': _mime2.default.getType(cacheFile),
                  'Last-Modified': stats.mtime.toGMTString()
                });
                res.end(data);
              }
            } else {
              // Enable retry for game api
              domain = req.headers.origin;
              pathname = parsed.pathname;
              requrl = req.url;
              let success = false;
              const retryConfig = _config2.default.get('proxy.retries', 0);
              const retries = retryConfig < 0 ? 0 : retryConfig;
              for (let i = 0; i <= retries; i++) {
                if (success) {
                  break;
                }
                // Delay 3s for retry
                if (i) {
                  yield _bluebird2.default.delay(3000);
                }
                try {
                  // Emit request event to plugins
                  reqBody = JSON.stringify(_querystring2.default.parse(reqBody.toString()));
                  _this.emit('network.on.request', req.method, [domain, pathname, requrl], reqBody, Date.now());
                  // Create remote request
                  const [response, body] = yield new Promise(_ref3);
                  success = true;
                  let resolvedBody = null;
                  // Emit response events to plugins
                  try {
                    resolvedBody = yield resolveBody(response.headers['content-encoding'], body);
                  } catch (e) {
                    // Unresolveable binary files are not retried
                    break;
                  }
                  if (resolvedBody === null) {
                    throw new Error('Empty Body');
                  }
                  if (response.statusCode == 200) {
                    _this.emit('network.on.response', req.method, [domain, pathname, requrl], JSON.stringify(resolvedBody), reqBody, Date.now());
                  } else {
                    _this.emit('network.error', [domain, pathname, requrl], response.statusCode);
                  }
                } catch (e) {
                  success = false;
                  (0, _utils.error)(`Connection failed: ${req.method} ${req.url} ${e.toString()}`);
                  if (i !== retries) {
                    _this.emit('network.error.retry', [domain, pathname, requrl], i + 1);
                  }
                }
                if (success || !isKancolleGameApi(pathname)) {
                  res.end();
                  break;
                }
              }
            }
          } catch (e) {
            (0, _utils.error)(`${req.method} ${req.url} ${e.toString()}`);
            _this.emit('network.error', [domain, pathname, requrl]);
          }
        }));
      });
      // HTTPS Requests
      this.server.on('connect', _ref8);
      this.server.on('error', _ref9);
      const listenPort = _config2.default.get('proxy.port', 0);
      this.server.listen(listenPort, _config2.default.get('proxy.allowLAN', false) ? '0.0.0.0' : '127.0.0.1', () => {
        this.port = this.server.address().port;
        _electron.app.commandLine.appendSwitch('proxy-server', `127.0.0.1:${this.port}`);
        _electron.app.commandLine.appendSwitch('ignore-certificate-errors');
        _electron.app.commandLine.appendSwitch('ssl-version-fallback-min', "tls1");
        (0, _utils.log)(`Proxy listening on ${this.port}`);
      });
    };

    this.load();
  }
}

exports.default = new Proxy();
module.exports = exports['default'];