"use strict";

exports.__esModule = true;
exports.default = void 0;

var _bluebird = _interopRequireDefault(require("bluebird"));

var _events = _interopRequireDefault(require("events"));

var _url = _interopRequireDefault(require("url"));

var _net = _interopRequireDefault(require("net"));

var _http = _interopRequireDefault(require("http"));

var _path = _interopRequireDefault(require("path"));

var _querystring = _interopRequireDefault(require("querystring"));

var _headerCaseNormalizer = _interopRequireDefault(require("header-case-normalizer"));

var _request = _interopRequireDefault(require("request"));

var _mime = _interopRequireDefault(require("mime"));

var _socks5Client = _interopRequireDefault(require("socks5-client"));

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

var _pacProxyAgent = _interopRequireDefault(require("pac-proxy-agent"));

var _electron = require("electron");

var _config = _interopRequireDefault(require("./config"));

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

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

function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }

const {
  ROOT
} = global;

const fs = require('fs-extra');

const zlib = _bluebird.default.promisifyAll(require('zlib'));

const resolveBody = async (encoding, body) => {
  let decoded = null;

  switch (encoding) {
    case 'gzip':
      decoded = await zlib.gunzipAsync(body);
      break;

    case 'deflate':
      decoded = await zlib.inflateAsync(body);
      break;

    default:
      decoded = body;
  }

  decoded = decoded.toString();

  if (decoded.indexOf('svdata=') === 0) {
    decoded = decoded.substring(7);
  }

  decoded = JSON.parse(decoded);
  return decoded;
};

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 = _config.default.get('poi.cachePath', global.DEFAULT_CACHE_PATH);

  return _path.default.join(dir, pathname);
};

const findHack = pathname => {
  let loc = getCachePath(_path.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(_path.default.join('Kanpani', pathname));
  } else if (hostname.match('assets.shiropro-re.net')) {
    loc = getCachePath(_path.default.join('ShiroPro', pathname));
  } else if (hostname.match('swordlogic.com')) {
    loc = getCachePath(_path.default.join('Shinken', pathname.replace(/^\/[0-9]{10}/, '')));
  } else if (hostname.match('dugrqaqinbtcq.cloudfront.net')) {
    loc = getCachePath(_path.default.join('FlowerKnightGirls', pathname));
  } else if (hostname.match('static.touken-ranbu.jp')) {
    loc = getCachePath(_path.default.join('ToukenRanbu', pathname));
  } else {
    loc = getCachePath(_path.default.join('KanColle', pathname));
  }

  try {
    fs.accessSync(loc, fs.R_OK);
    return loc;
  } catch (error) {
    return null;
  }
};

const PacAgents = {};

const resolve = req => {
  switch (_config.default.get('proxy.use')) {
    // HTTP Request via SOCKS5 proxy
    case 'socks5':
      return Object.assign(req, {
        agentClass: _Agent.default,
        agentOptions: {
          socksHost: _config.default.get('proxy.socks5.host', '127.0.0.1'),
          socksPort: _config.default.get('proxy.socks5.port', 1080)
        }
      });
    // HTTP Request via HTTP proxy

    case 'http':
      {
        const host = _config.default.get('proxy.http.host', '127.0.0.1');

        const port = _config.default.get('proxy.http.port', 8118);

        const requirePassword = _config.default.get('proxy.http.requirePassword', false);

        const username = _config.default.get('proxy.http.username', '');

        const password = _config.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 = _config.default.get('proxy.pacAddr');

        if (!PacAgents[uri]) {
          PacAgents[uri] = new _pacProxyAgent.default(uri);
        }

        return Object.assign(req, {
          agent: PacAgents[uri]
        });
      }
    // Directly

    default:
      return req;
  }
};

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

class Proxy extends _events.default {
  constructor() {
    super();
    Object.defineProperty(this, "serverInfo", {
      configurable: true,
      enumerable: true,
      writable: true,
      value: {}
    });
    Object.defineProperty(this, "getServerInfo", {
      configurable: true,
      enumerable: true,
      writable: true,
      value: () => this.serverInfo
    });
    Object.defineProperty(this, "load", {
      configurable: true,
      enumerable: true,
      writable: true,
      value: () => {
        const serverList = fs.readJsonSync(_path.default.join(ROOT, 'assets', 'data', 'server.json')); // HTTP Requests

        this.server = _http.default.createServer((req, res) => {
          delete req.headers['proxy-connection']; // Disable HTTP Keep-Alive

          req.headers['connection'] = 'close';

          const parsed = _url.default.parse(req.url);

          const isGameApi = parsed.pathname.startsWith('/kcsapi');

          if (isGameApi && this.serverInfo.ip !== parsed.hostname) {
            if (serverList[parsed.hostname]) {
              this.serverInfo = _extends({}, serverList[parsed.hostname], {
                ip: parsed.hostname
              });
            } else {
              this.serverInfo = {
                num: -1,
                name: '__UNKNOWN',
                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', async () => {
            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


              if (cacheFile) {
                const stats = await 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 = await fs.readFile(cacheFile);
                  res.writeHead(200, {
                    'Server': 'nginx',
                    'Content-Length': data.length,
                    'Content-Type': _mime.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 = _config.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) {
                    await _bluebird.default.delay(3000);
                  }

                  try {
                    // Emit request event to plugins
                    reqBody = JSON.stringify(_querystring.default.parse(reqBody.toString()));
                    this.emit('network.on.request', req.method, [domain, pathname, requrl], reqBody, Date.now()); // Create remote request

                    const [response, body] = await new Promise((promise_resolve, promise_reject) => {
                      (0, _request.default)(resolve(options), (err, res_response, res_body) => {
                        if (!err) {
                          promise_resolve([res_response, res_body]);
                        } else {
                          promise_reject(err);
                        }
                      }).pipe(res);
                    });
                    success = true;
                    let resolvedBody = null; // Emit response events to plugins

                    try {
                      resolvedBody = await 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', (req, client, head) => {
          delete req.headers['proxy-connection']; // Disable HTTP Keep-Alive

          req.headers['connection'] = 'close';

          const remoteUrl = _url.default.parse(`https://${req.url}`);

          let remote = null;

          switch (_config.default.get('proxy.use')) {
            case 'socks5':
              {
                // Write data directly to SOCKS5 proxy
                remote = _socks5Client.default.createConnection({
                  socksHost: _config.default.get('proxy.socks5.host', '127.0.0.1'),
                  socksPort: _config.default.get('proxy.socks5.port', 1080),
                  host: remoteUrl.hostname,
                  port: remoteUrl.port
                });
                remote.on('connect', () => {
                  client.write("HTTP/1.1 200 Connection Established\r\nConnection: close\r\n\r\n");
                  remote.write(head);
                });
                client.on('data', data => {
                  remote.write(data);
                });
                remote.on('data', data => {
                  client.write(data);
                });
                break;
              }

            case 'http':
              {
                // Write data directly to HTTP proxy
                const host = _config.default.get('proxy.http.host', '127.0.0.1');

                const port = _config.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, _headerCaseNormalizer.default)(k)}: ${req.headers[k]}\r\n`;
                }

                msg += "\r\n";
                remote = _net.default.connect(port, host, () => {
                  remote.write(msg);
                  remote.write(head);
                  client.pipe(remote);
                  remote.pipe(client);
                });
                break;
              }

            default:
              {
                // Connect to remote directly
                remote = _net.default.connect(remoteUrl.port, remoteUrl.hostname, () => {
                  client.write("HTTP/1.1 200 Connection Established\r\nConnection: close\r\n\r\n");
                  remote.write(head);
                  client.pipe(remote);
                  remote.pipe(client);
                });
              }
          }

          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();
          });
        });
        this.server.on('error', err => {
          (0, _utils.error)(err);
        });

        const listenPort = _config.default.get('proxy.port', 0);

        this.server.listen(listenPort, _config.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();
  }

}

var _default = new Proxy();

exports.default = _default;
module.exports = exports["default"];