#!/usr/bin/python3
#
# Copyright (C) 2017 Diebold Nixdorf
#
# This file is part of Warsaw Installer.
#
# Warsaw Installer is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Warsaw Installer is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Lesser Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Warsaw Installer.  If not, see <http://www.gnu.org/licenses/>.
#
# -*- coding: utf-8 -*-
#
import errno
import locale
import platform
import sys
import os
import stat
import time
import zipfile
import subprocess
import pwd
import re
import shutil
import tempfile
import gettext
from contextlib import closing, contextmanager

try:
    import urllib.request as urllib_request
    import urllib.parse as urllib_parser
except ImportError:
    import urllib2 as urllib_request
    from urllib2 import urlparse as urllib_parser

try:
    import io as StringIO
except ImportError:
    import StringIO

try:
    import gpgme
except ImportError:
    try:
        import gpg as gpgme
    except ImportError:
        pass

t = gettext.translation('warsaw', '/usr/share/locale', fallback=True)
try:
    _ = t.ugettext
except:
    _ = t.gettext


WARSAW_PACKAGE_PKEY='''
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1

mQINBFln6fsBEADHIzebJ7rJ270G96l9AkIUZmKyyQ9j0CtFCzYnCJ/jJ7AjLIsa
nL0d/GCNp3NMs6kLTwoGaaQTqPAe8IQXMDJVeYjZR5fbP9T7+8yYRBULHSeZI7CP
qjQbbKZ+5R+Vz/YOh2FXXj+Jlxcp2JsQcnnYCRJiBK8s59sSaoHuLT+nDaQbntwO
I2i+fLRiPhryhwHbiS2EackBV05/MqYkFjeRTWOXBhOmj52aXhqdycwduFDGKXop
sik3rFCUy1WLY37Kitn7JLLhhblmh1SwOOcozpXqNdRZXl7m4W+0L4ETEO1fuJds
0hxhjlqnMX/6yxWwLDqlr5eW2lWWBuzkPJL9xPw9ozlrlllsBcqkasqtdCyBQE/2
rV+00/a8fTeFcdvUqt6LoFW3/cT660hhI6i/0lLMzjLgUA5AeCoZXxA+qjswKPou
v5AsxobasfRiWQ+2adHkuyXwhBOBLn7wxBG5nmwhJm4bw8uZcUI890bCBI/5AU6U
9ZXL0fgcK8sEbgH1/613kFCd2nS768fM1Ndc4We1lmV83jOsF8ErG2JUHt5MDLX8
GYB6gw6P0tgVnXUzWQkoyIb2ZJ4fX3KIALMiUGgh+Bj8fhmEAKWzuinFX8Cu7/HR
DhpyrGIefqDoOK0Sbfw/p188jccfxm4RVvPYLbi2o+kMw+EF9+2IDNAdvwARAQAB
tChXYXJzYXcgTGludXggPHdhcnNhd0BkaWVib2xkbml4ZG9yZi5jb20+iQI4BBMB
AgAiBQJZZ+n7AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRAt/OursLgx
TJmTEACi1nh/WYYguDXikhhef588lv7TLICrSgK/bHswprud+VkGfyrX6QSfSbXV
r2kXdKBQt/av0rOQ+e5l6btLJiGUDxYPwtpa3qY+Azd/ktrSCoUC4nnkz2IxQzQJ
scCap1MgwrwG8HiEotD+WGXpkQGCCy3bjBvbE9g+xnk+XFTbXLYW21z4dCts4014
6ZZyQK72YoIuQ+IrNhEx5H1gnQRxEdfpucNDfxiuY5M6yFwVDdjD5i3B4H8zrt0z
55ee7chbppXYk5bWEVbhldA4dHvEYbgN5Y9FbOL5YZQRoIoGa9/jgX43ePwMrnP3
ACWc3fD6sFo2tlxm7tAwjsJM2gmQpl4kXVyTXS0Vr2+YA9FW4hBghbcPSCTI5tX5
xhh+4u5LJqUJabavfCWNt4nDPwrL/TcH5zFVchAKrNDCEdREKJVxaKE8ALF5lcRG
MgqbwjDoBWuA+/yATVVvEXJGwbX+JX9J1VomRAhD8L4y3NH0xrx/n7gRKcAgN898
UAAcrCKR7f/B2RkK3mWFQ8JeDiG+pPFGAYC1T98jB7PZYYYsBC9a6gEw9d2sJCnj
QPsyRVQc3K1wLMQENx6O0etK12EvjLiifuZ707vO3IxU+DEkBfEdcpsWL3/2nqD5
RjFGqKsfqFTy4qT8DYgjPQ4yPZ0ynoPbjIodZQ0cqiCS223QgQ==
=Mm2F
-----END PGP PUBLIC KEY BLOCK-----
'''


class WsConfig(object):
    def __init__(self, ws_install_prefix):
        self._prefix = ws_install_prefix
        self._warsaw = 'warsaw'
        self._config = os.path.join(self._prefix, 'etc', self._warsaw)
        self._bin = os.path.join(self._prefix, 'bin', self._warsaw)
        self._lib = os.path.join(self._prefix, 'lib', self._warsaw)
        self._rootca_common_name = 'Warsaw Personal CA'
        self._download_url = 'http://www.dieboldnixdorf.com.br/warsaw/'
        self._core_exec = os.path.join(self._bin, 'core')
        self._uninstaller_exec = os.path.join(self._bin, 'uninstall.sh')
    @property
    def ws_install_prefix(self):
        return self._prefix
    @property
    def download_url(self):
        return self._download_url
    @property
    def core_exec(self):
        return self._core_exec
    @property
    def uninstaller_exec(self):
        return self._uninstaller_exec
    @property
    def binaries(self):
        return self._bin
    @property
    def config(self):
        return self._config
    @property
    def libraries(self):
        return self._lib


command_list = dict()
options = WsConfig('/usr/local')


def set_default_encoding(fn):
    setattr(fn, 'default_encoding', locale.getpreferredencoding())
    return fn


@set_default_encoding
def unicode2str(s):
    if not isinstance(u'', str):
       s = s.encode(unicode2str.default_encoding)
    return s


def console_print(st=u'', f=sys.stdout, linebreak=True):
    cmd_line = [ 'zenity', '--info', '--text', unicode2str(st) ]
    try:
        if os.getuid() == 0:
            runas(get_calling_uid(), *cmd_line)
        else:
            runas(os.getuid(), *cmd_line)
        return
    except:
        pass
    f.write(st)
    if linebreak: f.write(os.linesep)


def read_input():
    return raw_input() if 'raw_input' in globals()['__builtins__'].keys() else input()


def yesno(question):
    status = -1
    cmd_line = [ 'zenity', '--question', '--text', question ]
    try:
        if os.getuid() == 0:
            status = runas(get_calling_uid(), *cmd_line)
        else:
            status = runas(os.getuid(), *cmd_line)
        return True if status == 0 else False
    except:
        pass

    while True:
        console_print(question, linebreak=False)
        console_print(_(u'[y/n]'), linebreak=False)
        a = read_input()
        if a.lower() in 'sy':
            return True
        elif a.lower() == 'n':
            return False
        else:
            console_print(_(u'Please you must type "y" if you agree or "n" otherwise'))


def get_calling_uid():
    uid = 0
    try:
        if 'SUDO_UID' in os.environ.keys():
            uid = int(os.getenv('SUDO_UID'))
        elif 'PACKAGEKIT_CALLER_UID' in os.environ.keys():
            uid = int(os.getenv('PACKAGEKIT_CALLER_UID'))
        elif 'USERNAME' in os.environ.keys():
            uid = pwd.getpwnam(os.getenv('USERNAME')).pw_uid
    except:
        pass
    return uid;


def get_system_arch():
    arch = platform.machine()
    pack_arch = ''
    if arch[0] == 'i' and arch[1].isdigit() and arch[2:4] == '86':
        pack_arch = 'x86_32'
    elif arch == 'x86_64':
        pack_arch = 'x86_64'
    return pack_arch


def download_link():
    info = [ 'warsaw_dist' ]
    dist = platform.dist()
    info.append(dist[0].lower()) if type(dist) == tuple else ''
    arch = get_system_arch()
    info.append(arch) if arch else ''
    return urllib_parser.urljoin(options.download_url, '_'.join(info) + '.zip')


def signature_link():
    return download_link() + '.sig'


def download_file_chunk(url, buf):
    proxy_support = urllib_request.ProxyHandler()
    opener = urllib_request.build_opener(proxy_support)
    opener.addheaders = [('User-Agent', "WarsawDownloader/1.12.6")]
    sock = opener.open(url)
    size = int(sock.info()['content-length'])
    bufsize = max(size / 200, 4096)
    progress = 0
    with closing(sock) as f:
        yield (0, True)
        while True:
            try: 
                chunk = f.read(bufsize)
                progress += len(chunk)
                buf.write(chunk)
                yield (float(progress)/size, True)
                if progress == size:
                    break
            except OSError as e:
                if hasattr(e, 'errno') and e.errno == errno.EAGAIN:
                    yield (float(progress)/size, False)
                else:
                    raise


@contextmanager
def gpgme_context(keys):
    _gpghome = tempfile.mkdtemp(prefix='tmp.gpghome')
    try:
        os.environ['GNUPGHOME'] = _gpghome
        with open(os.path.join(_gpghome, 'gpg.conf'), 'wb') as f:
            pass
        ctx = gpgme.Context()
        loaded = []
        for key_file in keys:
            result = ctx.import_(key_file)
            key = ctx.get_key(result.imports[0][0])
            loaded.append(key)
        ctx.signers = loaded
        yield ctx
    except:
        pass
    finally:
        del os.environ['GNUPGHOME']
        shutil.rmtree(_gpghome, ignore_errors=True)


def verify_signature(key_file, sig_file, plain_file):
    if not 'gpgme' in dir():
        if yesno(_(u'Unable to verify the content`s signature, python-gpgme package is not available in your system. Proceed anyway?')):
            return True
        return False;
    with gpgme_context([key_file]) as ctx:
        sigs = ctx.verify(sig_file, plain_file, None)
        return sigs[0].status == None


def extract_files(buf, prefix=''):
    z = zipfile.ZipFile(buf)
    total_entries = len(z.infolist())
    for i, entry in enumerate(z.infolist()):
        try:
            result = z.extract(entry, prefix)
        except:
            result = None
        yield entry.filename, float(i + 1) / total_entries, result != None
    z.close()


def install_proprietary_binaries():
    buf = StringIO.StringIO()
    sig = StringIO.StringIO()
    try:
        console_print(_(u'Downloading files') + ' ', linebreak=False)
        last = 0
        for progress, status in download_file_chunk(download_link(), buf):
            current = int(progress*100)
            if (current - last) >= 2:
                console_print(u".", linebreak=False)
                last = current
        console_print(u"")
        console_print(_(u'Verifying package signature.'))
        for progress, status in download_file_chunk(signature_link(), sig):
            pass
        buf.seek(0)
        sig.seek(0)
        if not verify_signature(StringIO.StringIO(WARSAW_PACKAGE_PKEY), sig, buf):
            raise
        buf.seek(0)
        console_print(_(u'Extracting files.'))
        for fname, progress, total_entries in extract_files(buf, options.ws_install_prefix):
            pass
    except:
        console_print(u"")
        return False
    if os.access(options.core_exec, os.W_OK):
        os.chmod(options.core_exec, 0o755)
    if os.access(options.uninstaller_exec, os.W_OK):
        os.chmod(options.uninstaller_exec, 0o755)
    os.system('command -v systemctl && systemctl enable warsaw || :')
    os.system('fc-cache -f')
    return True


def close_browsers():
    browser_pattern = '(firefox|chrome|opera|iceweasel|x-www-browser)'
    pids = runas3('pgrep', browser_pattern).split()
    if not pids:
        return True
    if not yesno(_(u'You must close all browser sessions before continue, want to close them now?')):
        return False
    runas3('pkill', '-TERM', browser_pattern)
    for i in range(10):
        pids = runas3('pgrep',browser_pattern).split()
        if not pids:
            return True
        time.sleep(1)
    return False


def start_warsaw():
    if is_warsaw_running():
        return True
    if os.access(options.core_exec, os.X_OK):
        core_process = subprocess.Popen([options.core_exec], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                preexec_fn=os.setsid, cwd='/', close_fds=True)
        for i in range(10):
            if is_warsaw_running():
                return True
            time.sleep(1)
    return False


def is_warsaw_running():
    uid = os.getuid()
    pids = runas3('pgrep', '-U', str(uid), '-f', '-x', options.core_exec).split()
    if pids:
        return True
    return False


def stop_warsaw():
    runas3('pkill', '-f', '-x', options.core_exec)
    for i in range(10):
        pids = runas3('pgrep', '-f', '-x', options.core_exec).split()
        if len(pids) == 0:
            break
        time.sleep(1)
    return True if len(pids) == 0 else False


def get_proc_directory():
    return '/proc'


def parse_environ(data):
    retval = dict()
    entries = map(lambda x: tuple(x.split('=',1)), data.split('\0'))
    for e in entries:
        try:
            retval.update(dict({e}))
        except ValueError:
            continue
    return retval


def get_environ(env_file):
    retval = dict()
    try:
        with open(env_file, 'r') as f:
            retval = parse_environ(f.read())
    except (UnicodeDecodeError, IOError):
        pass
    return retval;


def match_uid(uid):
    def match(pid_dir):
        try:
            st = os.lstat(pid_dir)
        except:
            return False
        return st.st_uid == uid if stat.S_ISDIR(st.st_mode) else False
    return match


def search_environ(uid, key, env = dict()):
    retval = ''
    old_dir = os.getcwd()
    os.chdir(get_proc_directory())
    pid_dirs = filter(match_uid(uid), os.listdir(get_proc_directory()))
    for pid_dir in pid_dirs:
        env.update(get_environ(os.path.join(get_proc_directory(), pid_dir, 'environ')))
        if key in env.keys():
            retval = env[key]
            break
    os.chdir(old_dir)
    return retval


def get_display(uid):
    result = search_environ(uid, 'DISPLAY')
    if result:
        return result
    return ':0'


def demote(to_uid):
    def run():
        if os.getuid() != 0:
            return
        try:
            os.setsid()
            os.setgid(pwd.getpwuid(to_uid).pw_gid)
            os.setuid(to_uid)
        except:
            pass
    return run


def load_user_environ(uid):
    env = dict()
    if os.getuid() != 0:
        env = os.environ.copy()
    if not 'DISPLAY' in env.keys():
        try:
            id_info = pwd.getpwuid(uid)
        except KeyError:
            pass
        search_environ(uid, 'DISPLAY', env)
        if not 'DISPLAY' in env.keys():
            env.update({
                'HOME': '/',
                'DISPLAY': ':0',
                'PATH': '/bin:/usr/bin'
            })
    return env


def runas3(*args):
    status = dict()
    runas2(os.getuid(), args, status)
    return  status['stdout'] + status['stderr']


@set_default_encoding
def runas2(uid, args, output = dict()):
    env = load_user_environ(uid)
    try:
        s = subprocess.Popen(args, preexec_fn=demote(uid), cwd='/', env=env,
            stderr=subprocess.PIPE, stdout=subprocess.PIPE)
        output['stdout'], output['stderr'] = s.communicate()
        s.wait()
        if not isinstance(output['stdout'], str):
            output['stdout'] = output['stdout'].decode(runas2.default_encoding)
            output['stderr'] = output['stderr'].decode(runas2.default_encoding)
        output['returncode'] = s.returncode
        return True
    except:
        output.update({ 'returncode': 1, 'stdout' : '', 'stderr' : '' })
    return False


def runas(uid, *args):
    status = dict()
    runas2(uid, args, status)
    return status['returncode']


def getpwnam_helper(line):
    uid = None
    m = re.match(r'([^\s]+).*', line)
    try:
        uid = pwd.getpwnam(m.group(1)).pw_uid if m else None
    except:
        pass
    return uid 


def get_usernames():
    who = [ x.strip() for x in os.popen('who','r').readlines() ]
    user_list = dict()
    for item in map(lambda x: getpwnam_helper(x), who):
        if item:
            user_list.update({ item : 1 })
    return user_list.keys()


def command(name):
    global command_list
    command_list[name.__name__] = name
    return name


@command
def stop(args):
    should_uninstall = '-r' in args
    if os.getuid() == 0:
        stop_warsaw()
        if should_uninstall and os.access(options.uninstaller_exec, os.X_OK):
            os.system(options.uninstaller_exec)


@command
def start(args):
    whoami = os.getuid()
    if not os.access(options.core_exec, os.X_OK):
        if whoami == 0:
            if not yesno(_(u'Warsaw will download some proprietary components now, continue?')):
                console_print(_(u'You still can install the missing components anytime, open a terminal and execute "sudo warsaw start"'))
                return 2
            if not install_proprietary_binaries():
                console_print(_(u'Unable download the proprietary binaries package, check your internet connection.'))
                return 2
        else:
            console_print(_(u'You must be logged in as root to install the proprietary packages.'))
            return 2
    if start_warsaw():
        if whoami == 0:
            for uid in get_usernames():
                try:
                    runas(uid, os.path.abspath(sys.argv[0]), 'start')
                except:
                    console_print(_(u'Unable to start one or more Warsaw user instances.'))
        return 0
    else:
        console_print(_(u'Unable to start Warsaw. You might want to try "sudo warsaw start".'))
    return 2


def main(argv):
    global options, command_list

    if not sys.platform.startswith('linux'):
        console_print(_(u'Operating system is not supported.'))
        sys.exit(ret)
    if len(argv) < 2 or argv[1] not in command_list:
        return 2
    locale.setlocale(locale.LC_ALL, '')
    return command_list[argv[1]](argv[2:])


if __name__ == "__main__":
    ret = main(sys.argv)
    if ret is not None:
        sys.exit(ret)

