#!/usr/bin/env python

from __future__ import print_function

"""
A simple client for SignServer

We contact a signing server, and for each file to be signed, send a
digest (actually the hex formatted digest) to be signed along with
user name and the path of the file that we hashed.
This limits the traffic over the network, and since the server sends
back a PEM encoded signature, the entire transaction is thus ascii.

We pay attention to the name we were invoked as, and look for
myname.cf to pickup defaults for the url to contact and which
hash to use.

Options:
    -c "config"
        load config from "config"

    -d  bump the debug level

    -u "url"
        connect to the given "url" (host:port)

    -h "hash"
        use "hash" to compute the digest we send to the server.
        The default (sha256) is sufficient for any reasonable
        signature method, though it is perhaps best to use the same
        method as the server.
        So if signing with RSA+SHA1, use sha1 as the digest to send.
        This is especially important if the server will use the digest
        directly.  If we send the wrong input it will return an error.

    -o "ofile"
        controls the file we will save the signature in.
        The default is '.', which means append the extension provided
        by the server to the file that was signed.
        Thus if signing '/some/file' and the server tells us to use
        '.sig' we will save the signature in '/some/file.sig'
        If "ofile" is '-' the signature will be written to stdout.

    --help
        Provilde a complete set of possible command-line options.	

The output of the --help command-line option provides the following:

usage: sign.py [--help] [-C CERTFILE] [-c CONFIG] [--crl CRLs] [-d]
               [-h {sha256,sha384,sha512,sha224,sha1}] [-o OFILE] [-r TRIES]
               [-u URL] [-T FILESFROM]
               [File [File ...]]

positional arguments:
  File                  File to be signed

optional arguments:
  --help                show this message
  -C CERTFILE           Certificate of public key
  -c CONFIG             Configuration file with defaults
  --crl CRLs		Request CRL data
  -d                    Add debugging output.
  -h {sha256,sha384,sha512,sha224,sha1}
                        Hashing algorithm to be used (default sha256).
  -o OFILE              Print sign output to ofile (default='.').
  -r TRIES              Number of times to retry server connections
  -u URL                Connect to the given "url" (host:port)
  -T FILESFROM, --files-from FILESFROM
                        get names to hash from FILE
"""

"""
RCSid:
	$Id: sign.py,v 1.97 2026/04/30 19:17:10 sjg Exp $

	@(#) Copyright (c) 2012-2026 Simon J. Gerraty
	@(#) Copyright (c) 2014, Juniper Networks, Inc.

	SPDX-License-Identifier: BSD-2-Clause

	Please send copies of changes and bug-fixes to:
	sjg@crufty.net

"""

import sys
import os.path
import socket
import time
import errno
import pwd

sys.dont_write_bytecode = True

# Python version 2 and version 3 compatibility functions
if sys.version_info[0] == 2:
    FileNotFoundError = IOError

    # b'str' and 'str' are equivalent
    # but not u'str'
    def b(s):
        if isinstance(s, unicode):
            return s.encode("latin-1")
        return s

    def s(b):
        if isinstance(b, unicode):
            return b.encode("latin-1")
        return b

else:
    # u'str' and 'str' are equivalent
    # but not b'str'
    def b(s):
        if isinstance(s, bytes):
            return s
        return s.encode("latin-1")

    def s(b):
        if isinstance(b, str):
            return b
        return b.decode("latin-1")


try:
    # try to get the fancy version
    from conf import initConfig, getBool, loadConfig
except:
    # this simpler version should be sufficient
    def initConfig(conf=None):
        if not conf:
            conf={}
        return conf

    def getBool(conf, name, default=False):
        if name in conf:
            x = conf[name]
            if isinstance(x, bool):
                return x
            if x[0] in '1TtYy':
                return True
            return False
        return default

    def loadConfig(name, conf=None, raise_not_found=True):
        """Add key value pairs into conf

        This is a very simple implementation compared to that in
        conf.py, but it allows this script to be useful by itself.
        
        We handle only the simple forms::

                key = value
                key ?= value
                key += value
                key := value

        if key already appears in conf, ``?=`` will not change it.

        Comments are skipped.

        The ``!=`` assignment operator, and lines starting
        with ``.`` are skipped with a warning.
        """
        if conf is None:
            conf = {}

        try:
            if not (name.startswith('/') or name.startswith('./')):
                dirs = [conf.get('progdir', '.')]
                if 'config_path' in conf:
                    for d in conf.get('config_path').split(','):
                        d = d.strip()
                        if not d in dirs:
                            dirs.append(d)
                if not '.' in dirs:
                    dirs.append('.')

                for d in dirs:
                    n = d + '/' + name
                    if os.path.exists(n):
                        name = n
                        break
            cf = open(name, 'r')
        except FileNotFoundError:
            if raise_not_found:
                raise
            return conf

        sep = conf.get('string_sep', ' ')

        for line in cf:
            if line.startswith('#'):
                continue
            if line.startswith('.'):
                print('Warning: ignoring: {}'.format(line.strip()), file=sys.stderr)
                continue
            x = line.find('=')
            if x > 1:
                if line[x+1] in '}]':
                    print('Warning: ignoring: {}'.format(line.strip()), file=sys.stderr)
                    continue
                if line[x-1] in ':?!+':
                    op = line[x-1:x+1]
                else:
                    op = '='
                k,v = line.split(op, 1)
                if op == '!=':
                    print('Warning: ignoring: {}'.format(line.strip()), file=sys.stderr)
                    continue
                k = k.strip()
                if op == '?=':
                    if k in conf:
                        continue
                elif op == '+=':
                    if k in conf:
                        conf[k] += sep + v.strip()
                        continue
                conf[k] = v.strip()
        return conf

# Useful utility functions
def basename(path, ext=None):
    """Return the basename of the path"""
    b = os.path.basename(path)
    if ext:
        x = b.rfind(ext)
        if x > 0:
            return b[0:x]
    return b

def bump_debug(conf, debug=0):
    """possibly increase debug level"""
    d = int(conf.get('debug', 0))
    if d > debug:
        debug = d
    return debug

def url2sock_address(url):
    """parse url and return appropriate socket family and address"""
    if url.startswith('/'):
        sock_family = socket.AF_UNIX
        connect_to = url
    else:
        sock_family = socket.AF_INET
        host = None
        if url.find(':') >= 0:
            (host,port) = url.split(':')
            port = int(port)
        else:
            port = int(url)
        if port < 0 or port > 65535:
            raise ValueError('0 <= port <= 65535')
        if not host:
            host = 'localhost'

        connect_to = (host,port)
    return (sock_family,connect_to)

default_ip = os.getenv('SIGNER_DEFAULT_IP')
# sadly there is no consistency with EAI_*
# these are the cases we consier retryable
retryable_eai = []
for e in ['EAI_AGAIN','EAI_NODATA','EAI_NONAME']:
    if e in socket.__dict__:
        retryable_eai.append(socket.__dict__[e])

def retryable_connect_error(e, connect_to):
    """return a description of error and whether retry makes sense"""
    m = ': {0}: {1}: {2}'.format(e.errno, connect_to, e.strerror)
    # most errors are not worth retrying
    rc = False
    if isinstance(e, socket.gaierror):
        # Linux defines these differently to BSD
        if e.errno in retryable_eai:
            rc = True
    elif isinstance(e, socket.herror):
        if e.errno in [2,4]:
            rc = True
    else:
        import errno

        if e.errno in [errno.ETIMEDOUT]:
            rc = True

    return(rc,m)

def urlconnect(addrinfo,tries=1,debug=0):
    """connect socket using addrinfo"""
    nai = len(addrinfo)
    for i in range(tries):
        em = ''
        for x,(sock_family,sock_type,sock_proto,canname,connect_to) in enumerate(addrinfo):
            try:
                sock = socket.socket(sock_family, sock_type, sock_proto)
                if debug:
                    sys.stderr.write('trying {} ...\n'.format(connect_to))
                sock.connect(connect_to)
                return (sock,'')
            except socket.error as e:
                rc,m = retryable_connect_error(e, connect_to)
                if debug:
                    # m starts with ':'
                    sys.stderr.write('caught{}. retryable={}\n'.format(m,rc))
                if x < nai - 1:
                    continue
                # this was our last addrinfo
                if not rc:
                    em = 'ERROR'+m
                    if sock:
                        sock.close()
                    return (None,em)

        secs = i % 8
        if secs > 0:
            time.sleep(secs)
        if secs == 0 and i + 1 < tries:
            sys.stderr.write('WARNING'+m+'. retrying...\n')
        if sock:
            sock.close()
            sock = None
    return (sock,em)

def timestamp(fmt='@ %s [%Y-%m-%d %T]'):
    """format current time"""
    return time.strftime(fmt)

def error(msg, exit=1):
    """Print an error message to stderr and exit"""
    p = 'ERROR: '
    if msg.startswith('ERROR'):
        p = ''
    sys.stderr.write('{} {}{}\n'.format(timestamp(), p, msg))
    sys.exit(exit)

def parse_options(argv, xoptf=None, conf=None, filterfileargs=None, debug=0):
    """Parse the command-line options
    Usage:
      script [-C certs][-c config][-d][-h hash][-o outfile]
             [-r tries][-u url][--help][file [file]...]
    """
    import argparse

    conf = initConfig(conf)
    # set defaults in conf rather than via argparse
    # since we cannot otherwise tell the difference between a default
    # and a command line override
    conf['hash'] = 'sha256'
    conf['tries'] = 10
    if '.Vars' in conf:
        conf = loadConfig('${progdir}/${progname:R}.cf', conf, False)
        conf = loadConfig('./${progname:R}.cf', conf, False)
    else:
        Myname = basename(sys.argv[0], '.py')
        Mydir = os.path.dirname(sys.argv[0])

        conf['progdir'] = Mydir
        conf['progname'] = Myname
        myconf = Myname + '.cf'
        conf = loadConfig(myconf, conf, False)

    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument('--help', action="help", help="show this message")
    parser.add_argument('-C', dest='certfile',
                        help="Certificate of public key")
    parser.add_argument('-c', dest='config', default=None,
                        help="Configuration file with defaults")
    parser.add_argument('--crl', dest='crlfile',
                        help="CRL data for CAs")
    parser.add_argument('--ta', dest='tafile',
                        help="Trust Anchor (rootCA)")
    parser.add_argument('-d', dest='debug', action='count',
                        default=argparse.SUPPRESS,
                        help="Add debugging output.")
    parser.add_argument('-h', dest='hash',
                        choices=['sha256', 'sha384', 'sha512', 'sha224',
                                 'sha1'],
                        help="Hashing algorithm to be used (default {}).".format(conf['hash']))
    parser.add_argument('-o', dest='ofile', default='.',
                        help="Print sign output to ofile (default='.').")
    parser.add_argument('-r', dest='tries',
                        help="Number of times to retry server connections (default {})".format(conf['tries']))
    parser.add_argument('-u', dest='url',
                        help='Connect to the given "url" (host:port)')
    if not 'user' in conf:
        parser.add_argument('-x', action='store_true',
                            help="Username is sent with signing request")
    parser.add_argument('files', metavar='File',
                        help="File to be signed", nargs='*')
    parser.add_argument('-T', '--files-from', dest='filesfrom',
                        help="get names to hash from FILE")

    # Add additional parser options
    if xoptf:
        xoptf(parser, conf)

    args = parser.parse_args(argv[1:])

    d = vars(args)

    # Gather the filenames to be hashed from the command-line
    if d['files']:
        if filterfileargs:
            files = []
            for f in d['files']:
                if filterfileargs(f):
                    files.append(f)
        else:
            files = d['files']
        del d['files']
    else:
        files = []

    # Load a config file
    if d['config']:
        conf = loadConfig(d['config'], conf)
        del d['config']

    # Command-line overrides a loaded configuration
    for k in d:
        if d[k]:
            conf[k] = d[k]

    # If there are a huge number of files to be signed, it is useful
    # to read the list from either stdin or a file rather than on the
    # command line.
    if 'certfile' in conf and 'filesfrom' in conf:
        error('incompatible arguments: -C {} -T {}'.format(d['certfile'], d['filesfrom']))
    elif 'certfile' in conf and len(files) > 0:
        error('incompatible arguments: -C {} and files: {}'.format(d['certfile'], files))
    elif 'filesfrom' in conf:
        if conf['filesfrom'] == '-':
            # read stdin as a list of files one per line
            readfiles = sys.stdin
        else:
            # read files to hash from provided file
            readfiles = open(conf['filesfrom'], 'r')
        for line in readfiles:
            # Note: We are explicitly NOT checking these files with
            # filterfileargs() callback. We would expect the user to
            # use grep or some other stream filter for that case.
            files.append(line.strip())
        readfiles.close()

    # Potentially increase the current debug level
    debug = bump_debug(conf, debug)

    if debug:
        print("==== begin config =====", file=sys.stderr)
        for k,v in conf.items():
            print('conf[{}]={}'.format(k, v), file=sys.stderr)
        print("==== end config =====", file=sys.stderr)

    if debug:
        print("===== begin argument list =====", file=sys.stderr)
        for f in files:
            print('{}'.format(f), file=sys.stderr)
        print('===== end argument list =====', file=sys.stderr)

    return (conf, files)

def write_signature(sig, fname, ofile, ext, debug=0):
    """Write a signature to the output file"""

    if debug > 1:
        print("\n==> write_signature: fname='{}', ofile='{}', ext='{}')\n".format(fname, ofile, ext), file=sys.stderr)

    if ofile == '-':
        print(s(sig), end='')
    else:
        if ofile == '.':
            if not ext:
                error("No signature extention defined.")
            sfile = fname + ext
        elif ofile.startswith('.'):
            sfile = fname + ofile
        else:
            sfile = ofile
        try:
            os.unlink(sfile)
        except:
            pass
        open(sfile, 'wb').write(sig)

        if debug:
            print("wrote {} bytes into {}".format(len(sig), sfile), file=sys.stderr)

class SignClient(object):
    """base Sign Client"""

    def __init__(self, conf={}, debug=0):
        """client setup"""
        self.conf = conf
        port = conf.get('ListenPort', 6161)
        self.url = conf.get('url', 'localhost:{}'.format(port))
        hname = conf.get('hash', 'sha256')
        m = __import__('hashlib', fromlist=[hname])
        self.hashfunc = getattr(m, hname)
        self.diag_func = None
        self.hname = hname
        self.debug = bump_debug(conf, debug)
        self.recv_size = int(conf.get('recv_size', 4096))
        self.always_loop = getBool(conf, 'always_loop')
        self.tries = int(conf.get('tries', 10))
        self.sign_log_keys = self.conf.get('SignLogKeys', 'user,path').split(',')
        self.sock = None
        self.peername = None
        self.msg_flags = []
        self.resolve_url()

    def run_diagnostics(self, reason):
        """ run diagnostics if configured
        look for ${diagnostics} function in ${dianostics}.py
        we pass it reason connect_to and addrinfo (may not be set).
        """

        if self.diag_func is None:
            diagnostics = self.conf.get('diagnostics')
            if diagnostics is None:
                return

            self.diag_ran = {}
            try:
                diag_module = __import__(diagnostics)
                diag_func = getattr(diag_module, diagnostics)
                self.diag_func = diag_func
            except BaseException as err:
                import traceback
                traceback.print_exc()

        # we don't want to over do this
        if reason in self.diag_ran:
            return
        self.diag_ran[reason] = True

        try:
            self.diag_func(reason, self.connect_to, self.addrinfo, self.conf)
        except BaseException as err:
            import traceback
            traceback.print_exc()

    def resolve_url(self, tries=3):
        """resolve url"""
        self.sock_family,self.connect_to = url2sock_address(self.url)
        if self.sock_family == socket.AF_UNIX:
            self.addrinfo = [(self.sock_family,socket.SOCK_STREAM,0,'',self.connect_to)]
            return
        name,port = self.connect_to
        default_addrinfo = self.addrinfo = None
        if default_ip:
            try:
                default_addrinfo = socket.getaddrinfo(default_ip,port,0,socket.SOCK_STREAM)
            except socket.gaierror as e:
                if self.debug:
                    sys.stderr.write('caught: {}: {}: {}\n'.format(e.errno,default_ip,e.strerror))
                pass

        for i in range(tries):
            try:
                # use 0 rather than self.sock_family
                # in case there are AF_INET6 addresses
                self.addrinfo = socket.getaddrinfo(name,port,0,socket.SOCK_STREAM)
                if default_addrinfo and name[0].isdigit() and name != default_ip:
                    self.addrinfo += default_addrinfo
                return
            except socket.gaierror as e:
                rc,m = retryable_connect_error(e, self.connect_to)
                if self.debug:
                    # m starts with :
                    sys.stderr.write('caught{}. retryable={}\n'.format(m, rc))
                self.run_diagnostics('resolve')
                if not rc or (default_addrinfo and i + 1 == tries):
                    if default_addrinfo:
                        self.msg_flags.append('dnsfail')
                        self.addrinfo = default_addrinfo
                        return
                    else:
                        raise

            secs = i % 8
            if secs > 0:
                time.sleep(secs)
            if secs == 0 and i + 1 < tries:
                sys.stderr.write('WARNING'+m+'. retrying...\n')

        if self.addrinfo is None:
            raise

    def hash_file(self, file):
        """hash file using the indicated method
        If file.$hash exists and is up to date,
        we will use its content.
        """
        h = self.hashfunc()
        hfile = '{}.{}'.format(file, h.name)
        try:
            sth = os.stat(hfile)
            # size of hfile needs to be what we expect
            if sth.st_size == (1 + 2 * h.digest_size):
                # and it needs to be up to date
                stf = os.stat(file)
                if sth.st_mtime >= stf.st_mtime:
                    f = open(hfile, 'r')
                    x = f.read()
                    f.close()
                    return x
        except:
            pass
        f = open(file, 'rb')
        for line in f:
            h.update(line)
        f.close()
        return h.hexdigest()+'\n'

    def server_name(self):
        if self.peername:
            return '{} [{}]'.format(self.url, self.peername[0])
        else:
            return self.url

    def send_command_try(self, msg, debug=0):
        """Attempt to send a command to the server and return the response

        We should be called within try block
        """

        sock = self.sock
        if not sock:
            sock,response = urlconnect(self.addrinfo,
                                       int(self.conf.get('urlconnect_tries',3)),
                                       self.debug)
            if sock:
                self.peername = sock.getpeername()
            else:
                return response
        sock.send(b(msg))

        # Read from the server and accumulate as needed.
        # Certificate chains, CRL data etc can be more than recv_size.
        # For Certificate chains etc, always loop until EOF.
        # If we are only looping because of always_loop, break the loop if
        # we receive less than recv_size bytes.
        need_eof = msg.startswith('certs:') or msg.startswith('crls:') or \
                   msg.startswith('ta:')
        if self.always_loop or need_eof:
            # CRL data or
            # certificate hierarchy may not fit in one response
            r = []
            response = None
            while True:
                result = sock.recv(self.recv_size)
                if not result:
                    break
                result = s(result)
                r.append(result)
                if len(result) < self.recv_size and not need_eof:
                    break
            response = ''.join(r)
        else:
            # maximum signature is a PEM signature of a 4096
            # bit key used by Microsoft UEFI signer. In base64
            # this will need around 16 lines of around 65
            # characters, less than 2K bytes.
            # Even double that should easily fit in the default
            # recv_size of 4k
            response = sock.recv(self.recv_size)
            response = s(response)

        self.sock = sock
        return response

    def send_command(self, msg, debug=0):
        """Send a command to the server and return the response

        Calls send_command_try to do the actual deed so we can capture
        exceptions and retry as needed.
        """

        if self.debug > debug:
            debug = self.debug
        if debug:
            print('sending cmd: {}'.format(msg), file=sys.stderr)

        tries = self.tries
        for i in range(tries):
            try:
                response = self.send_command_try(msg, debug)
                if response:
                    break
            except ValueError as e:
                response = 'ERROR: {0}: {1}'.format(self.server_name(), str(e))
                break
            except socket.error as e:
                # XXX might need a list or errno's for which
                # retry is pointless
                if hasattr(e, 'errno'):
                    response = 'ERROR: {0}: {1}: {2}'.format(e.errno, self.server_name(), e.strerror)
                else:
                    response = 'ERROR: {0}: {1}'.format(self.server_name(), str(e))
                    break

                self.run_diagnostics('connect')

                secs = i % 8
                if secs > 0:
                    time.sleep(secs)
                if secs == 0 and i + 1 < tries:
                    sys.stderr.write(response + '. retrying...\n')
                if self.sock:
                    self.sock.close()
                    self.sock = None

        if not response:
            # server may have rejected us due to acl
            response = 'ERROR: no response from: {} for {}'.format(self.server_name(), msg)

        if response.startswith('ERROR'):
            self.run_diagnostics('connect')
            error(response)

        return response

    def cleanup(self):
        """cleanup client state"""
        if self.sock:
            self.sock.close()
            self.sock = None

    def get_data(self, key, req):
        """fetch the certs file and write it out"""
        if not self.conf[key]:
            return

        response = self.send_command(req)

        dFile = None
        if self.conf[key] == '-':
            dFile = sys.stdout
        else:
            try:
                os.unlink(self.conf[key])
            except:
                pass
            dFile = open(self.conf[key], 'wb')
            response = b(response)

        dFile.write(response)
        dFile.close()
        self.cleanup()

    def get_certs(self):
        """fetch the certs file and write it out"""
        self.get_data('certfile', 'certs:')

    def get_crls(self):
        """fetch the CRL data and write it out"""
        self.get_data('crlfile', 'crls:')

    def get_ta(self):
        """fetch the Trust Anchor data and write it out"""
        self.get_data('tafile', 'ta:')

    def sign(self, path, myhash, debug=0):
        """Sign the hash."""
        if len(self.msg_flags) > 0:
            msg = 'flags={} '.format(','.join(self.msg_flags))
        else:
            msg = ''
        for k in self.sign_log_keys:
            if k in self.conf:
                msg += '{}={} '.format(k, self.conf[k])
        msg += 'path={} hash={}'.format(path, myhash)
        signature = response = self.send_command(msg, debug)

        # The signature will potentially have a set of atribute-value
        # pairs. Seprate them
        avl = {}
        if response.startswith('#set:'):
            knobs,signature = response.split('\n', 1)
            knobs = s(knobs)
            for knob in knobs.split()[1:]:
                k,v = knob.split('=', 1)
                avl[k] = v

        return (avl, signature)

def main(klass=SignClient, xoptf=None, conf=None, filterfileargs=None, debug=0):
    """Simple driver for class SignClient or derivatives.

    """

    conf = initConfig(conf)
    user = uid = os.getuid()
    # on some platforms getpwuid can be unreliable
    # log uid in such cases
    try:
        user = pwd.getpwuid(uid)[0]
    except:
        pass
    conf['user'] = user

    conf, args = parse_options(sys.argv, xoptf, conf, filterfileargs, debug=0)
    client = klass(conf)
    if 'certfile' in conf and conf['certfile']:
        # ignore the argument list
        client.get_certs()
        if args:
            print('Warning: Processed certs request. - ignoring files:')
            print(*args, sep=' ')
    elif 'crlfile' in conf and conf['crlfile']:
        # ignore the argument list
        client.get_crls()
        if args:
            print('Warning: Processed CRL request. - ignoring files:')
            print(*args, sep=' ')
    elif 'tafile' in conf and conf['tafile']:
        # ignore the argument list
        client.get_ta()
        if args:
            print('Warning: Processed Trust Anchor request. - ignoring files:')
            print(*args, sep=' ')
    else:
        debug = bump_debug(conf, debug)
        ofile = conf.get('ofile', '.')
        # Default to letting the main or the server tell us the suffix
        # to use.
        sig_ext = conf.get('sig_ext', None)

        # Some hashes may take a long time and it is better to fully
        # occupy a worker thread rather than make it wait for hashing
        # operations on the client side. So, loop through the files to
        # hash them and then loop through the files to sign them.
        if debug:
            print('===== begin hashing {} files ====='.format(len(args)), file=sys.stderr)
        hashes = {}
        for f in args:
            hashes[f] = client.hash_file(f)
        if debug:
            print('===== end hashing {} files ====='.format(len(args)), file=sys.stderr)
            print('===== begin signing hashes =====', file=sys.stderr)

        for f in args:
            knobs, signature = client.sign(f, hashes[f], debug)

            # This will generally never set the signature extension
            if ofile == '.' and not sig_ext:
                # the only one we care about
                if 'sig_ext' in knobs:
                    sig_ext = knobs['sig_ext']

            signature = b(signature)
            write_signature(signature, f, ofile, sig_ext, debug)

        if debug:
            print('===== end signing hashes =====', file=sys.stderr)
    pass

if __name__ == '__main__':
    try:
        main(SignClient)
    except SystemExit:
        raise
    except:
        # yes, this goes to stdout
        print("ERROR: {}".format(sys.exc_info()[1]))
        raise
    sys.exit(0)
