// LICENSE_CODE ZON
'use strict'; /*jslint node:true*/
require('../util/config.js');
const conf = require('../util/conf.js');
// XXX vladimir: -DNODE_PLATFORM="android"
conf.app = 'svc_js_'+({linux: 'android'}[process.platform]||process.platform);
const agent_proxy = require('./unblocker/agent_proxy.js');
const unblocker_lib = require('./pub/unblocker_lib.js');
const smart_proxy = require('./unblocker/smart_proxy.js');
const lum_proxy = require('./unblocker/lum_proxy.js');
const transparent = require('./unblocker/transparent.js');
const wget = require('../util/wget.js');
const zerr = require('../util/zerr.js');
const string = require('../util/string.js');
const file = require('../util/file.js');
const hash = require('../util/hash.js');
const etask = require('../util/etask.js');
const exec = require('../util/exec.js');
const date = require('../util/date.js');
const zutil = require('../util/util.js');
const zescape = require('../util/escape.js');
const zconf = require('../util/config.js');
const client = require('./peer/client.js');
const _ = require('lodash');
const os = require('os');
const crypto = require('crypto');
const E = exports, assign = Object.assign;
let confdir;

if (process.env.ZNODE_DEP_SCAN)
{
    let dep_scan = require('../util/dep_scan.js');
    process.stdout.write(JSON.stringify(dep_scan()));
    process.exit(0);
}

process.zon.init();
// XXX volodymyr: svc lives long, still need to implement real storage
unblocker_lib.fake_storage = {};
unblocker_lib.init({
    perr: opt=>zerr.perr(opt.id, zutil.omit(opt, ['id']), {retry: true}),
    ajax: {json: opt=>etask(function*(){
        let uri = zescape.uri(opt.url, opt.qs);
        let res = yield wget({url: uri, method: 'GET',
            timeout: opt.timeout, body: opt.data, json: true,
            expect: 200, retry: opt.retry});
        return res.resp.body;
    })},
    storage: {
        get: id=>unblocker_lib.fake_storage[id],
        set: (id, val)=>unblocker_lib.fake_storage[id] = val,
    },
    get_auth: ()=>{
        // XXX volodymyr: cid/session_key is dynamic and not reliable (e.g.
        // wont work between vpn reconnections), need to improve js conn
        // authentication method (for now using fake key)
        let cid = E.get_cid_js();
        return {product: 'svc', cid,
            session_key: hash.hash_string(cid.replace(/^\d+-/, ''))};
    },
    get_ver: ()=>({product: 'svc', svc_ver: zconf.ZON_VERSION}),
});

E.get_cid_js = ()=>client.cid_js||'';
E.get_session_key_js = ()=>client.session_key_js||'';
E.is_connected = ()=>client.connected>0;
E.has_internet = ()=>client.has_internet>0;

E.proxy_get_port = function(country, force_port, use_luminati, user, zone,
    password){
    if (use_luminati)
        return lum_proxy.get(country, user, zone, password).port;
    return smart_proxy.get(country, force_port).port; };

E.set_auth_info = agent_proxy.set_auth_info;
E.should_redirect_outgoing_tcp = transparent.should_redirect_outgoing_tcp;
E.enable_app_unblocker = transparent.enable;
E.disable_app_unblocker = transparent.disable;
E.set_apkid = client.set_apkid;
E.set_appid = client.set_appid;
E.set_partnerid = client.set_partnerid;
E.set_sdk_version = client.set_sdk_version;
E.set_idle = client.set_idle;
E.set_confdir = dir=>{
    confdir = dir;
    client.set_confdir(dir);
};
E.set_os_ver = client.set_os_ver;
E.zid_update = client.zid_update;
E.set_mobile_enable = client.set_mobile_enable;
E.set_premium_mode = function(mode){
    unblocker_lib.set_premium_mode(mode);
    client.set_premium_mode(mode);
};
E.set_product = function(prod){
    zerr.notice(`set product to ${prod}`);
    agent_proxy.set_product(prod);
};
E.set_paid_mode = function(mode){
    E.reset_proxies();
    agent_proxy.set_paid_mode(mode);
};
E.set_app_trial_mode = function(mode){
    E.reset_proxies();
    unblocker_lib.set_app_trial_mode(mode);
};
E.set_client_conf = client.set_client_conf;
E.usage_update = client.usage_update;
E.notify_idle_state = client.notify_idle_state;
E.set_user_token = client.set_user_token;
E.get_user_token = client.get_user_token;
E.start_connection = transparent.start_connection;
E.stop_connection = transparent.stop_connection;
E.get_and_flush = transparent.get_and_flush;
E.unblocker_json_set = function(json, opt){
    unblocker_lib.unblocker_json_set(json, opt);
    transparent.unblocker_json_set(json, opt);
};
E.perr_wakeup = client.perr_wakeup;

E.reset_proxies = function(){ smart_proxy.reset(); };

let last_gw;
E.gw_change_notify = function(gw){
    if (gw==last_gw)
        return;
    zerr.notice('Gateway IP changed from '+last_gw+' to '+gw);
    let is_link_recovered = last_gw=='0.0.0.0';
    last_gw = gw;
    smart_proxy.purge_stats();
    unblocker_lib.cache_purge();
    client.connect(gw);
    if (is_link_recovered && connected_ts && connected_to)
        vpn_reconnect(connected_to);
};
E.connect_no_monitor = client.connect;

let connecting_to, reconnecting_to, connected_to, disconnecting;
let connecting_et, reconnecting_et;
let connecting_ts, reconnecting_ts, connected_ts;
let last_error, last_error_text;
let update_vpn_status = ()=>etask(function*(){
    let res = (yield exec.sys('rasdial', {out: 'stdout'}))||'';
    let m = res.match(/hola_vpn_(\w*)/);
    let last_connected_to = connected_to;
    connected_to = m&&m[1];
    if (last_connected_to && !connected_to && connected_ts)
    {
        zerr.perr('vpn_tunnel_disconnect_n', {country: last_connected_to,
            duration: ms_since(connected_ts), autodisconnect: true},
            {retry: true});
        connected_ts = 0;
        zerr.notice('vpn autodisconnected from %s, reconnecting...',
            last_connected_to);
        vpn_reconnect(last_connected_to);
    }
});

let ms_since = d=>date()-d;

let monitor_vpn_status = ()=>{
    E.vpn_monitor = E.vpn_monitor || etask.interval(1000, function*(){
        yield update_vpn_status();
    });
};

let ps_run = cmd=>etask(function*(){
    return yield exec.sys(['powershell', '-ExecutionPolicy', 'Bypass',
        '-NoLogo', '-NonInteractive', '-NoProfile', '-WindowStyle', 'Hidden',
        '-Command', cmd], {out: 'ret', stdout: true});
});

let ps_cmd_exists = cl=>etask(function*(){
    let res = yield ps_run('Get-Command '+cl+' -errorAction SilentlyContinue');
    return !res.status_code;
});

let ps_conn_test_available = ()=>etask(function*(){
    return yield ps_cmd_exists('Test-Connection'); });

let ps_conn_test = server=>etask(function*(){
    let res = yield ps_run('Test-Connection -Count 2 -Quiet '+server);
    return {status_code: res.status_code,
        success: res.stdout.startsWith('True')};
});

let ps_dns_cache_get = lookup=>etask(function*(){
    let record, records = [];
    let res = yield ps_run('ipconfig /displaydns');
    if (res.status_code)
        return records;
    res.stdout.split('\n').forEach(line=>{
        let m;
        if (m = line.match(/Record Name/))
            record = {};
        else if (!record)
            return;
        if (m = line.match(/^\s*(.*?)[\s\.]*:\s*(.*?)\s*$/))
        {
            let field;
            let d2f = {
                'Record Name': 'name',
                'Record Type': 'type',
                'Time To Live': 'ttl',
                'A (Host) Record': 'ip',
            };
            if (field = d2f[m[1]])
                record[field] = m[2];
        }
        else if (/^\s*$/.test(line))
        {
            records.push(record);
            record = undefined;
        }
    });
    return lookup ? records.filter(r=>r.name==lookup) : records;
});

let ps_dns_cache_clear = ()=>etask(function*(){
    return yield ps_run('ipconfig /flushdns'); });

let vpn_conn_test = agent=>etask(function*(){
    let res = {attempts: 0, failed: [], status: 'timeout'};
    this.alarm(date.ms.MIN, {return: res});
    if (!(yield ps_conn_test_available()))
        return assign(res, {status: 'skip_no_avail'});
    while (res.attempts++<5)
    {
        yield ps_dns_cache_clear();
        let test = yield ps_conn_test(agent);
        // use best effort if test command was unable to execute
        if (test.status_code)
            return assign(res, {status: 'skip_test_exception'});
        let records = yield ps_dns_cache_get(agent);
        if (test.success)
        {
            if (res.attempts>1)
            {
                zerr.perr('vpn_no_internet_fixed', {agent, dnscache: records,
                    failed: res.failed, attempt: res.attempts,
                    fixed: records.length ? records[0].ip : 'unknown'},
                    {retry: true});
            }
            return assign(res, {status: 'success'});
        }
        else if (res.attempts==1)
        {
            zerr.perr('vpn_no_internet', {agent, dnscache: records,
                failed: records.length ? records[0].ip : 'unknown'},
                {retry: true});
        }
        if (!records.length)
            return assign(res, {status: 'skip_no_dns'});
        if (!res.failed.includes(records[0].ip))
            res.failed.push(records[0].ip);
        if (!records.find(r=>!res.failed.includes(r.ip)))
            return assign(res, {status: 'failed_all_ips'});
        // assuming next dns request will rotate ips
    }
    return assign(res, {status: 'failed_too_many_iter'});
});

let vpn_conn_test_safe = agent=>etask(function*(){
    try { return yield vpn_conn_test(agent); }
    catch(e){ return {status: 'skip_js_error', err: zerr.e2s(e)}; }
});

// fallback to xx.vpn.hola.org: pick zagent from dns resolv result
let vpn_agent_get_fallback = opt=>etask(function*(){
    // XXX volodymyr: for now win10 only, powershell gets stuck on earlier
    // versions (win7..win8 to be added later)
    if (os.release().match(/^10\./))
    {
        zerr.notice('checking server connectivity to: '+opt.host);
        let test = yield vpn_conn_test_safe(opt.host);
        zerr.notice('connectivity test completed: %s after %d attempts%s',
            test.status, test.attempts||0, test.failed && test.failed.length ?
            ' failed on '+test.failed.join(',') : '');
        if (test.status!='success')
        {
            zerr.perr('vpn_tunnel_connect_test_failed',
                assign({agent: opt.host}, test), {retry: true});
        }
    }
    return opt.host;
});

let vpn_agent_get = opt=>etask(function*(){
    let {country} = opt;
    let last_req = zutil.obj_pluck(vpn_agent_get, 'last_req')||{};
    let rmt_conf = client.get_rmt_conf();
    if (!rmt_conf.enable_vpn_agents_lib)
        return yield vpn_agent_get_fallback(opt);
    try {
        let res = last_req.country!=country ?
            yield unblocker_lib.get_agents(null, {country}) :
            yield unblocker_lib.change_agents(null, {country},
            [last_req.agent]);
        let agents = zutil.get(res, `verify_proxy.${country}`, []);
        let agent = zutil.get(agents[0], 'agent', {});
        if (!agent.host)
            throw {reason: 'no agents found', data: res};
        if (agent.type!='vpn')
            throw {reason: 'non-vpn agents selected', data: res};
        vpn_agent_get.last_req = {country, agent};
        zerr.perr('vpn_agent_get_lib', {country, agent}, {retry: true});
        // XXX volodymyr: strongswan is configured with certificate
        // issued for *.vpn.hola.org, need to fix /zgettunnels instead
        if (/zagent\d+\.hola\.org/.test(agent.host))
           return agent.host.replace('hola.org', 'vpn.hola.org');
        return agent.host;
    }
    catch(e){
        zerr.perr('vpn_agent_get_fallback', {country, error: e},
            {retry: true});
        return yield vpn_agent_get_fallback(opt);
    }
});

let vpn_reconnect = country=>etask(function*(){
    let attempt = 1;
    reconnecting_et = this;
    reconnecting_to = country;
    reconnecting_ts = date();
    this.finally(()=>
        reconnecting_et = reconnecting_to = reconnecting_ts = null);
    for(;;)
    {
        while (last_gw=='0.0.0.0')
            yield etask.sleep(0.5*date.ms.SEC);
        zerr.notice(`vpn reconnect to ${country} retry ${attempt}`);
        yield E.vpn_connect({reconnect: attempt++});
        if (connected_to)
            break;
        yield etask.sleep(10*date.ms.SEC);
    }
});

E.vpn_connect = opt=>etask(function*(){
    opt = !opt.reconnect ? E.vpn_connect.last_opt = opt :
        assign({reconnect: opt.reconnect}, E.vpn_connect.last_opt);
    if (disconnecting)
        return;
    last_error = null;
    last_error_text = null;
    yield E.vpn_disconnect({reconnect: opt.reconnect});
    connecting_to = opt.country;
    connecting_et = this;
    this.on('uncaught', e=>{
        zerr.notice('vpn_connect failed with '+zerr.e2s(e));
        zerr.perr('vpn_tunnel_connect_error_n', {country: opt.country,
            exception: zerr.e2s(e)}, {retry: true});
    });
    this.finally(()=>connecting_to = connecting_ts = connecting_et = null);
    connecting_ts = date();
    zerr.perr('vpn_tunnel_connect_try_n', {country: opt.country,
        auto: opt.auto, reconnect: opt.reconnect}, {retry: true});
    let agent = yield vpn_agent_get(opt);
    let agent_lookup = ms_since(connecting_ts);
    let pbk_path = os.tmpdir()+'/hola_vpn.pbk';
    let name = 'hola_vpn_'+opt.country;
    try {
        // static guid is needed to prevent windows from creating a new
        // network profile each time we connect to vpn
        file.write_e(pbk_path, string.align`
            [${name}]
            Guid=5629C625B992485C810A48D3004E37D7
            Type=2
            VpnStrategy=7
            MEDIA=rastapi
            Port=VPN2-0
            Device=WAN Miniport (IKEv2)
            DEVICE=vpn
            PhoneNumber=${agent}
            IpPrioritizeRemote=1
            Ipv6PrioritizeRemote=1`);
    } catch(e){
        zerr.err('pbk file error: '+zerr.e2s(e));
        return;
    }
    zerr.notice(`connecting to ${opt.country} vpn server: ${agent}`);
    disconnecting = false;
    let res = yield exec.sys(['rasdial', name, opt.username, opt.password,
        '/phonebook:'+pbk_path], {out: 'ret', stdout: true});
    last_error = res.status_code;
    let duration = ms_since(connecting_ts);
    if (last_error)
    {
        let m = res.stdout.match(/\d{3} - (.*)$/m);
        last_error_text = m ? m[1] : res.stdout;
        zerr.notice('vpn connection error '+last_error+': '+last_error_text);
        zerr.perr('vpn_tunnel_connect_error_n', {country: opt.country, agent,
            agent_lookup, duration, code: last_error, desc: last_error_text,
            auto: opt.auto, reconnect: opt.reconnect}, {retry: true});
    }
    else
    {
        last_error_text = null;
        connected_to = opt.country;
        connected_ts = date();
        zerr.notice('vpn connected');
        zerr.perr('vpn_tunnel_connect_n', {country: opt.country, agent,
            agent_lookup, duration, auto: opt.auto, reconnect: opt.reconnect},
            {retry: true});
    }
    monitor_vpn_status();
});

E.vpn_disconnect = opt=>etask(function*(){
    opt = opt||{};
    last_error = null;
    last_error_text = null;
    yield update_vpn_status();
    // close reconnect loop only by external disconnect call
    if (!opt.reconnect && (connecting_et || reconnecting_et))
    {
        zerr.perr('vpn_tunnel_connect_interrupt_n', {
            country: reconnecting_to||connecting_to,
            duration: ms_since(reconnecting_ts||connecting_ts),
            reconnect: opt.reconnect}, {retry: true});
        if (connecting_et)
            connecting_et.return();
        if (reconnecting_et)
            reconnecting_et.return();
    }
    else if (connected_ts)
    {
        zerr.perr('vpn_tunnel_disconnect_n', {country: connected_to,
            duration: ms_since(connected_ts)}, {retry: true});
        connected_ts = 0;
    }
    if (disconnecting||!connected_to)
        return;
    disconnecting = true;
    this.finally(()=>disconnecting = false);
    let name = 'hola_vpn_'+connected_to;
    zerr.notice('disconnecting vpn: '+name);
    let res = yield exec.sys(['rasdial', name, '/d'],
        {out: 'ret', stdout: true});
    last_error = res.status_code;
    if (last_error)
    {
        let m = res.stdout.match(/\d{3} - (.*)$/m);
        last_error_text = m ? m[1] : res.stdout;
        zerr.notice('vpn disconnection error '+last_error+': '+
            last_error_text);
    }
    else
    {
        last_error_text = null;
        connected_to = null;
        zerr.notice('vpn disconnected');
    }
    monitor_vpn_status();
});

E.is_vpn_supported = ()=>os.platform().startsWith('win') &&
    !!os.release().match(/^10|6\.3|6\.2|6\.1/);

E.vpn_status = ()=>{
    if (!E.is_vpn_supported())
        return {};
    monitor_vpn_status();
    // we silently attempt to reconnect without any feedback to the user
    if (reconnecting_et)
    {
        return {connected_to: reconnecting_to,
            reconnecting: !!reconnecting_et};
    }
    if (connecting_to||disconnecting)
        return {connecting_to, disconnecting};
    return {connected_to, last_error, last_error_text};
};

let user_settings = {
    auto_connect: false,
    auto_connect_country: 'us',
};
const crypt_alg = 'aes-256-ctr', crypt_pass = 'eA2cS1gO';

let encrypt = str=>{
    let cipher = crypto.createCipher(crypt_alg, crypt_pass);
    let res = cipher.update(str, 'utf8', 'hex');
    res += cipher.final('hex');
    return res;
};

let decrypt = str=>{
    let decipher = crypto.createDecipher(crypt_alg, crypt_pass);
    let res = decipher.update(str, 'hex', 'utf8');
    res += decipher.final('utf8');
    return res;
};

let load_user_settings = ()=>{
    zerr.notice('load_user_settings');
    try {
        let settings = file.read(confdir+'/db/user_settings.json');
        if (settings)
            user_settings = JSON.parse(settings);
    } catch(e){ zerr.err('load_user_settings failed: '+zerr.e2s(e)); }
};

let get_auth_data = ()=>{
    zerr.notice('get_auth_data');
    try {
        let auth;
        if (auth = file.read(confdir+'/db/data.dat'))
            return JSON.parse(decrypt(auth));
    } catch(e){ zerr.err('get_auth_data failed: '+zerr.e2s(e)); }
};

E.vpn_get_settings = ()=>{
    zerr.notice('vpn_get_settings');
    return user_settings;
};

E.vpn_set_settings = opt=>{
    zerr.notice('vpn_set_settings');
    let auth = _.pick(opt, 'username', 'password');
    let sett = _.omit(opt, 'username', 'password');
    _.each(sett, (val, key)=>
        sett[key] = val=='true' ? true : val=='false' ? false : val);
    assign(user_settings, sett);
    file.write(confdir+'/db/user_settings.json',
        JSON.stringify(user_settings));
    if (!_.isEmpty(auth))
        file.write(confdir+'/db/data.dat', encrypt(JSON.stringify(auth)));
    return user_settings;
};

E.vpn_auto_connect = ()=>etask(function*(){
    if (!confdir)
    {
        zerr.err('no confdir');
        return;
    }
    load_user_settings();
    let country = user_settings.auto_connect_country;
    if (!user_settings.auto_connect || !country)
    {
        zerr.notice('auto connect disabled');
        return;
    }
    let auth_data = get_auth_data();
    if (!auth_data)
    {
        zerr.err('no auth data');
        return;
    }
    if (connected_to || connecting_to)
    {
        zerr.notice('already '+(connected_to ? 'connected ' : 'connecting ')+
            'to '+(connected_to || connecting_to));
        return;
    }
    country = country.toLowerCase();
    zerr.notice('auto connect to '+country);
    yield E.vpn_connect(assign({
        country,
        host: country+'.vpn.hola.org',
        auto: true,
    }, _.pick(auth_data, 'username', 'password')));
    zerr.perr('vpn_auto_connect',
        {country, success: !!connected_to, error: last_error}, {retry: true});
});

E.vpn_ui_app_loaded = ()=>etask(function*(){
    zerr.notice('vpn_ui_app_loaded');
    if (!E.is_vpn_supported())
        return;
    yield E.vpn_auto_connect();
});

// XXX vladimir: process.exit() stops libuv event loop even if
// there are unclosed timers/tasks/handles. Find all of them and fix.
process.on('SIGTERM', ()=>{
    process.exit(0);
});

const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');

function local_ui(){
    if (!(this instanceof local_ui))
        return new local_ui();
    this.server = http.createServer((req, res)=>{
        try {
            let params = url.parse(req.url, true);
            if (this.serve_local(req, res, params.pathname))
                return;
            this.proxy_req(req, res, params);
        } catch(e){ zerr.err('local_ui server: '+zerr.e2s(e)); }
    }).listen();
    this.server.once('listening', ()=>{
        this.port = this.server.address().port;
        zerr.notice('local_ui server on port '+this.port);
    });
}
local_ui.prototype.proxy_req = function(req, res, params){
    let options = {
        hostname: params.hostname,
        port: params.port||80,
        path: params.path,
        method: req.method,
        headers: req.headers,
    };
    let proxy = http.request(options, pres=>{
        res.writeHead(pres.statusCode, pres.headers);
        pres.pipe(res, {end: true});
    });
    proxy.on('error', e=>zerr.err('local_ui proxy: '+zerr.e2s(e)));
    req.pipe(proxy, {end: true});
};
local_ui.prototype.is_allowed = p=>['/vpn_config', '/vpn_settings']
    .includes(p);
local_ui.prototype.serve_local = function(req, res, pathname){
    if (req.method!='GET')
        return;
    let ref = req.headers.referer||req.headers.referrer||'';
    let params = ref ? url.parse(ref) : {};
    let fpath = confdir+'/app/ui'+(this.is_allowed(pathname) ?
        pathname+'_static.html' : pathname);
    if (!this.is_allowed(pathname) && !this.is_allowed(params.pathname) &&
        params.hostname!='hola.org' || !file.exists(fpath))
    {
        return;
    }
    // XXX nikita: use express
    let type, ext = path.extname(fpath);
    switch (ext)
    {
    case '.html': type = 'text/html'; break;
    case '.css': type = 'text/css'; break;
    case '.js': type = 'application/javascript'; break;
    case '.json': type = 'application/json'; break;
    case '.svg': type = 'image/svg+xml'; break;
    case '.jpeg': type = 'image/jpeg'; break;
    case '.png': type = 'image/png'; break;
    case '.ico': type = 'image/x-icon'; break;
    default: type = 'text/html'; break;
    }
    try {
        let s = fs.createReadStream(fpath);
        s.on('open', ()=>{
            res.setHeader('Content-Type', type);
            s.pipe(res);
        });
        s.on('error', ()=>{
            res.setHeader('Content-Type', 'text/plain');
            res.statusCode = 404;
            res.end('Not found');
        });
    } catch(e){ zerr.err('local_ui: '+zerr.e2s(e)); }
    return true;
};
local_ui.prototype.close = function(){
    if (!this.server)
        return;
    this.server = void this.server.close();
};

let app_ui;
E.start_ui = ()=>{
    if (!confdir)
        return zerr.err('start_ui: no confdir');
    app_ui = new local_ui();
    process.on('exit', code=>app_ui.close());
};

E.ui_get_port = ()=>app_ui && app_ui.port ? app_ui.port : -1;
