#!/usr/bin/python3
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2010  Rafal Wojtczuk  <rafal@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
import os
import os.path
import stat
import re
import sys
import subprocess
import shutil
import grp
import qubesadmin
import tempfile

updates_dir = "/var/lib/qubes/updates"
updates_rpm_dir = updates_dir + "/rpm"
updates_repodata_dir = updates_dir + "/repodata"
updates_error_file = updates_dir + "/errors"

comps_file = None
if os.path.exists('/usr/share/qubes/Qubes-comps.xml'):
    comps_file = '/usr/share/qubes/Qubes-comps.xml'

package_regex = re.compile(r"\A[A-Za-z0-9][A-Za-z0-9._+^~-]{0,255}\.rpm\Z")
# example valid outputs:
#  .....rpm: digests signatures OK
# example INVALID outputs:
#  .....rpm: sha1 md5 OK
#  .....rpm: RSA sha1 ((MD5) PGP) md5 NOT OK (MISSING KEYS: (MD5) PGP#246110c1)
#  .....rpm: digests OK
# example of valid outputs from old RPM (not supported anymore):
#  .....rpm: rsa sha1 (md5) pgp md5 OK
#  .....rpm: (sha1) dsa sha1 md5 gpg OK
gpg_ok_suffix = b": digests signatures OK\n"


def dom0updates_fatal(msg):
    print(msg, file=sys.stderr)
    with open(updates_error_file, "a") as updates_error_file_handle:
        updates_error_file_handle.write(msg + "\n")
    shutil.rmtree(updates_rpm_dir)
    exit(1)


def handle_dom0updates(updatevm):
    source = os.getenv("QREXEC_REMOTE_DOMAIN")
    if source != updatevm.name:
        print('Domain ' + str(source) + ' not allowed to send dom0 updates',
            file=sys.stderr)
        exit(1)
    # Clean old packages
    if os.path.exists(updates_rpm_dir):
        shutil.rmtree(updates_rpm_dir)
    if os.path.exists(updates_repodata_dir):
        shutil.rmtree(updates_repodata_dir)
    if os.path.exists(updates_error_file):
        os.remove(updates_error_file)
    qubes_gid = grp.getgrnam('qubes').gr_gid
    old_umask = os.umask(0o002)
    os.mkdir(updates_rpm_dir)
    os.chown(updates_rpm_dir, -1, qubes_gid)
    os.chmod(updates_rpm_dir, 0o0775)
    try:
        with tempfile.TemporaryDirectory(
                dir='/var/tmp',
                prefix='qubes-updates-tmp',
                suffix='.UNTRUSTED') as tmp_dir:
            subprocess.check_call(["/usr/libexec/qubes/qfile-dom0-unpacker",
                str(os.getuid()), tmp_dir, '--only-regular-files'])
            # Verify received files
            for untrusted_f in os.listdir(tmp_dir):
                if not package_regex.match(untrusted_f):
                    raise Exception(
                        'Domain ' + source + ' sent unexpected file')
                f = untrusted_f
                assert '/' not in f
                assert '\0' not in f
                assert '\x1b' not in f

                tmp_full_path = tmp_dir + "/" + f
                full_path = updates_rpm_dir + "/" + f
                # lstat does not dereference symbolic links
                if not stat.S_ISREG(os.lstat(tmp_full_path).st_mode):
                    raise Exception(
                        'Domain ' + source + ' sent not regular file')
                try:
                    subprocess.check_call((
                        'rpmcanon', '--allow-old-pkgs', '--',
                        tmp_full_path, full_path))
                except subprocess.CalledProcessError:
                    raise Exception('Error canonicalizing ' + tmp_full_path)
                os.unlink(tmp_full_path)
                # _pkgverify_level all: force digest + signature verification
                # _pkgverify_flags 0x0: force all signatures and digests to be checked
                rpm_argv = ("rpmkeys", "-K", "--define=_pkgverify_level all",
                            "--define=_pkgverify_flags 0x0", "--", full_path)
                # rpmkeys output is localized, sigh
                p = subprocess.Popen(rpm_argv,
                         executable='/usr/bin/rpmkeys',
                         env={'LC_ALL': 'C'},
                         cwd='/',
                         stdin=subprocess.DEVNULL,
                         stdout=subprocess.PIPE)
                output = p.communicate()[0]
                if p.returncode != 0:
                    raise Exception(
                        'Error while verifying %s signature: %s' % (f, output))
                if output != full_path.encode('ascii', 'strict') + gpg_ok_suffix:
                    raise Exception(
                        'Domain ' + source + ' sent not signed rpm: ' + f)
    except Exception as e:
        dom0updates_fatal(str(e))
    # After updates received - create repo metadata
    createrepo_cmd = ["/usr/bin/createrepo_c"]
    if comps_file:
        createrepo_cmd += ["-g", comps_file]
    createrepo_cmd += ["-q", updates_dir]
    subprocess.check_call(createrepo_cmd)
    os.chown(updates_repodata_dir, -1, qubes_gid)
    os.chmod(updates_repodata_dir, 0o0775)
    exit(0)


def main():
    app = qubesadmin.Qubes()
    
    updatevm = app.updatevm
    if updatevm is None:
        exit(1)
    handle_dom0updates(updatevm)


if __name__ == '__main__':
    main()
