Mini Shell

Direktori : /opt/imh-python/lib/python3.9/site-packages/ngxconf/
Upload File :
Current File : //opt/imh-python/lib/python3.9/site-packages/ngxconf/builder.py

# vim: set ts=4 sw=4 expandtab syntax=python:
"""

ngxconf.builder
Configuration builder functions

Copyright (c) 2017-2020 InMotion Hosting, Inc.
http://www.inmotionhosting.com/

@author J. Hipps <jacobh@inmotionhosting.com>

"""

import os
import sys
import codecs
import re
import pwd
import json
import logging
import signal
from glob import glob

import yaml
from yaml import CDumper, CLoader
import arrow
import pem
import netifaces

from ngxconf import __version__, __date__, default_conf, default_header
from ngxconf import fpm, cpfpm
from ngxconf.util import load_template, gconf, dict_strip_mtime, cp_get_package_data, cp_get_userinfo, get_profile

logger = logging.getLogger('ngxconf')
udefs = None

def rebuild_all(force=False, defaults=False, skip_fpm=False, userdef=None):
    """
    Rebuild configuration for all users
    """
    global udefs
    udefs = userdef
    ngx_changes = []
    fpm_changes = []

    # Register signal handler & check for/register lock
    signal.signal(signal.SIGINT, sigint_handler)
    signal.signal(signal.SIGUSR1, sigusr_handler)
    check_lock(kill_previous=True)

    # Build default/catch-all server block, if enabled
    if gconf.enable_catchall:
        build_default()

    # Enumerate cPanel users
    try:
        ulist = os.listdir("/var/cpanel/users")
        logger.debug("Enumerated %d users", len(ulist))
    except Exception as e:
        logger.error("Failed to enumerate users from /var/cpanel/users: %s", str(e))
        return None

    # Load template
    t_server = load_template('user_server')

    # Rebuild config for each user
    for tuser in ulist:
        # Skip invalid users
        if tuser in ['system']:
            logger.debug("Skipping invalid user '%s'", tuser)
            continue
        nchg = rebuild_user(tuser, force=force, template=t_server, defaults=defaults, skip_fpm=skip_fpm)
        if nchg:
            ngx_changes += nchg['nginx']
            fpm_changes += nchg['fpm']

    # Purge config files for users who no longer exist
    clist = set()
    try:
        for tfile in os.listdir("/etc/nginx/vhosts"):
            clist.add(tfile.split('__')[0])
    except Exception as e:
        logger.error("Failed to enumerate vhost includes: %s", str(e))

    nlist = clist - set(ulist)
    for tuser in nlist:
        try:
            tglob = glob("/etc/nginx/vhosts/%s__*.conf" % (tuser))
        except Exception as e:
            logger.error("Failed to enumerate vhost includes for %s: %s", tuser, str(e))
            continue

        for tfile in tglob:
            try:
                os.unlink(tfile)
                logger.debug("Removed stale file: %s", tfile)
                fpm_changes.append(tfile.split('__', 1)[1].replace('_', '.'))
            except Exception as e:
                logger.error("Failed to remove stale Nginx include %s: %s", tfile, str(e))
                continue

    # Finish up: clear lock & return number of changes
    logger.info("Rebuilt configuration for %d users -- CHANGED: Nginx: %d / FPM: %d",
                len(ulist), len(ngx_changes), len(fpm_changes))
    clear_lock()
    return {'nginx': ngx_changes, 'fpm': fpm_changes}

def rebuild_user(user, force=False, outfile=None, template=None, defaults=False, skip_fpm=False, userdef=None):
    """
    Rebuild configuration for single user
    If @defaults is True, reverts all user configs to defaults
    """
    global udefs
    if userdef:
        udefs = userdef

    # Parse cPanel userdata
    udata = parse_userdata(user)
    if udata is None:
        logger.error("Rebuild of user '%s' failed", user)
        return None

    # Parse user info file
    uinfo = cp_get_userinfo(user)
    if uinfo is None:
        logger.error("update_user: aborting due to invalid user info")
        return None

    # Parse user configuration (creating default configs if the don't exist)
    uconf = parse_userconf(user, udata, uinfo, defaults=defaults, force=force)

    # Update php-fpm configuration
    if gconf.enable_fpm and not skip_fpm:
        if gconf.fpm_management.lower() == 'ngxconf':
            fpm_updates = fpm.update_user(user, udata, uconf, uinfo, force=force)
        elif gconf.fpm_management.lower() == 'cpanel':
            vlist = [x for x in udata.keys() if not x.startswith('_') and not uconf.get(x, {}).get('_exclude', False)]
            fpm_updates = cpfpm.update_fpm(user, vlist, force=force)
        else:
            logger.error("FPM CONFIGURATION NOT BUILT: Unrecognized `fpm_system` selection '%s'", gconf.fpm_system)
            fpm_updates = []

        if len(fpm_updates) > 0:
            logger.info("PHP-FPM configuration updated for %s [%s]", user, ', '.join(fpm_updates))
    else:
        fpm_updates = []

    # Get mtimes of last nginx conf builds
    utime = get_last_nconf_updates(user)

    # Load template
    if template is None:
        t_server = load_template('user_server')
    else:
        t_server = template

    # Ensure user cache zones exist
    try:
        cache_path = '%s/%s' % (gconf.cache_base_path, user)
        os.stat(cache_path)
    except:
        try:
            os.mkdir(cache_path)
        except Exception as e:
            logger.error("Unable to create user cache zone directory: %s", str(e))

    # Ensure user cache zone perms/ownership
    try:
        ngx_pwd = pwd.getpwnam(gconf.nginx_user)
        os.chmod(cache_path, 0o0775)
        os.chown(cache_path, ngx_pwd.pw_uid, ngx_pwd.pw_gid)
    except Exception as e:
        logger.error("Unable to set user cache zone perms/ownership: %s", str(e))

    # Build Nginx config for each domain, if it has changed (or force=True)
    # tconf = user config, tdat = cpanel userdata, tdom = domain
    ngx_updates = []
    for tdom, tdat in udata.items():
        if tdom.startswith('_'):
            continue
        tconf = uconf.get(tdom)
        if tconf:
            # Check for user-defined domain includes
            uinclude = None
            if gconf.allow_user_includes:
                try:
                    incpath = '/home/%s/.imh/nginx/%s.inc.conf' % (user, tdom.replace('.', '_'))
                    if os.path.exists(incpath):
                        with open(incpath, 'r') as f:
                            uinclude = f.read()
                            logger.debug("Read %d lines from user include %s", len(uinclude.splitlines()), incpath)
                    else:
                        logger.debug("No user include found at %s", incpath)
                except Exception as e:
                    logger.error("Failed to read user include file at [%s]: %s", incpath, str(e))

            # Check to see if the userdata _SSL file was changed
            try:
                ssl_uconf_changed = utime.get(tdom, 0) < udata[tdom]['_ssl']['_mtime']
            except:
                ssl_uconf_changed = False

            try:
                if tconf.get('_exclude') is True:
                    logger.debug("Excluding %s:%s", user, tdom)
                    try:
                        outconf = "/etc/nginx/vhosts/%s__%s.conf" % (user, tdom.replace('.', '_'))
                        os.unlink(outconf)
                    except:
                        pass
                elif force or (utime.get(tdom, 0) < udata[tdom]['_mtime']) \
                         or (utime.get(tdom, 0) < uconf[tdom]['_mtime']) \
                         or (utime.get(tdom, 0) < int(os.stat(f"/var/cpanel/users/{user}").st_mtime)):
                    # render user nginx config from template
                    b_main = (tdom == udata['_main']['main_domain'])
                    ngx_updates.append(tdom)
                    if outfile:
                        render_config(user, tdom, tconf, tdat, t_server, is_main=b_main, basepath=outfile, userinc=uinclude)
                    else:
                        render_config(user, tdom, tconf, tdat, t_server, is_main=b_main, userinc=uinclude)
                else:
                    logger.debug("Skipping %s:%s -- up-to-date", user, tdom)
            except Exception as e:
                logger.error("Render for %s:%s failed: %s", user, tdom, str(e))
        else:
            logger.warning("Skipping rebuild for %s:%s due to invalid data", user, tdom)

    # Remove config files for any domains that have been removed
    for tdom in utime.keys():
        if not udata.get(tdom):
            tpath = "/etc/nginx/vhosts/%s__%s.conf" % (user, tdom.replace('.', '_'))
            try:
                os.unlink(tpath)
                logger.debug("Removed stale file: %s", tpath)
                fpm_updates.append(tdom)
            except Exception as e:
                logger.error("Unable to remove stale config %s: %s", tpath, str(e))
    return {'nginx': ngx_updates, 'fpm': fpm_updates}

def build_default(outconf='/etc/nginx/conf.d/default.conf', use_ipv6=False):
    """
    Build default.conf from template
    """
    template = load_template('default_server')

    # get list of IPs
    iplist = []
    ilraw = map(lambda x: map(lambda y: y.get('addr'),
                netifaces.ifaddresses(x).get(netifaces.AF_INET, [])),
                netifaces.interfaces())
    if use_ipv6:
        ilraw += map(lambda x: map(lambda y: y.get('addr'),
                     netifaces.ifaddresses(x).get(netifaces.AF_INET6, [])),
                     netifaces.interfaces())
    for tip in ilraw:
        iplist += tip

    # filter out unwanted IPs
    iplist = list(set([x for x in iplist if not re.match(r'^(127\.0\.0\.|::1|fe80|fd00)', x)]))
    logger.debug("Detected %d IPs on the server: %s", len(iplist), ', '.join(iplist))

    # determine main IP
    try:
        with open('/var/cpanel/mainip', 'r') as f:
            main_ip = f.read().strip()
    except Exception as e:
        logger.error("Failed to read main IP from /var/cpanel/mainip: %s", str(e))
        main_ip = iplist[0]
    logger.debug("Got main IP: %s", main_ip)

    # write system service ssl pem/key
    dconf = {'ssl_certificate': None, 'ssl_key': None}
    cpath = '/var/cpanel/ssl/cpanel/mycpanel.pem'
    dconf['ssl_certificate'], dconf['ssl_key'] = create_cert_from_combined('mycpanel', cpath)

    # render with Jinja2
    try:
        ttext = template.render(tstamp=arrow.now().format(), ips=iplist, dconf=dconf,
                                tls_cipher_list=gconf.tls_cipher_list, main_ip=main_ip,
                                ngxconf_ver="%s (%s)" % (__version__, __date__))
    except Exception as e:
        logger.error("Failed to render default config: %s", str(e))
        return False

    # write new file
    try:
        with open(outconf, 'w') as f:
            f.write(ttext)
        logger.debug("Rendered default config to %s", outconf)
    except Exception as e:
        logger.error("Failed to render default config to file %s: %s", outconf, str(e))
        return False

    return True


def render_config(user, userdom, userconf, userdata, template, is_main=False, basepath='/etc/nginx/vhosts', userinc=None):
    """
    Compile template using Jinja2
    """
    outconf = "%s/%s__%s.conf" % (basepath, user, userdom.replace('.', '_'))

    # create list of cPanel domains
    cpdomains = {'cpanel': [], 'webmail': []}
    try:
        for talias in userdata['serveralias'].split():
            if not talias.startswith("www.") and not talias.startswith("mail."):
                cpdomains['cpanel'].append("cpanel." + talias)
                cpdomains['webmail'].append("webmail." + talias)
    except Exception as e:
        logger.error("Failed to create list of cPanel subdomains for %s: %s", userdom, str(e))
        cpdomains = {'cpanel': None, 'webmail': None}

    # render with Jinja2
    try:
        ttext = template.render(tstamp=arrow.now().format(), domain=userdom, cache_name=user, is_main=is_main,
                                cache_key_format=gconf.cache_key_format, user=user, uconf=userconf,
                                http_ip=gconf.http_listen_interface, cp_domains=cpdomains,
                                rl_whitelist=gconf.use_ratelimit_whitelist, restrict_purge=gconf.restrict_purge,
                                tls_cipher_list=gconf.tls_cipher_list, udata=userdata,
                                user_include=userinc, cpanel_host_redirect=gconf.cpanel_host_redirect,
                                manage_tls_options=gconf.manage_tls_options,
                                ngxconf_ver="%s (%s)" % (__version__, __date__))
    except Exception as e:
        logger.error("Failed to render template for %s:%s: %s", user, userdom, str(e))
        return False

    # write new include file
    try:
        with codecs.open(outconf, 'w', 'utf-8', errors='ignore') as f:
            f.write(ttext)
        logger.debug("Rendered config for %s:%s to %s", user, userdom, outconf)
    except Exception as e:
        logger.error("Failed to render template to file %s: %s", outconf, str(e))
        return False

    return True

def parse_userconf(user, udata, uinfo, defaults=False, force=False):
    """
    Parse Nginx user config files
    Default configuration will be created if missing
    """
    global udefs

    idir = '/home/%s/.imh' % (user)
    udir = '/home/%s/.imh/nginx' % (user)

    # Check for config dir
    try:
        os.stat(udir)
    except:
        try:
            os.makedirs(udir, mode=0o0775)
            logger.debug("Created directory %s", udir)
        except Exception as e:
            logger.error("Unable to create directory %s: %s", udir, str(e))
            return False

    if udata is None:
        logger.error("No userdata associated with '%s' account; skipping", udata)
        return None

    # Get user UID/GID
    try:
        u_pwd = pwd.getpwnam(user)
    except Exception as e:
        logger.error("Failed to translate username '%s' to UID/GID: %s", user, str(e))
        return None

    # Ensure ~/.imh dir perms/ownership
    try:
        os.chmod(idir, 0o0775)
        os.chown(idir, u_pwd.pw_uid, u_pwd.pw_gid)
    except Exception as e:
        logger.warning("Unable to set permissions/ownership of %s: %s", udir, str(e))

    # Ensure nginx dir perms/ownership
    try:
        os.chmod(udir, 0o0775)
        os.chown(udir, u_pwd.pw_uid, u_pwd.pw_gid)
    except Exception as e:
        logger.warning("Unable to set permissions/ownership of %s: %s", udir, str(e))

    package_data = cp_get_package_data(uinfo.get('PLAN'))
    tprofile = package_data.get('IMH_NGX_PROFILE')

    ucdata = {}
    for tdom, tdat in udata.items():
        if tdom.startswith('_'):
            continue
        tfile = '%s/%s.yml' % (udir, tdom.replace('.', '_'))

        # Check if file exists; if not, create with default contents
        try:
            ustat = os.stat(tfile)
        except Exception:
            ustat = None

        # Also revert back to defaults if @defaults is True
        if ustat is None or defaults:
            logger.debug("Copying default userconf to %s", tfile)
            create_default_userconf(user, tfile, tdat, profile=tprofile)
            try:
                ustat = os.stat(tfile)
            except Exception as e:
                logger.error("Userconf not found (creation failed) %s: %s", tfile, str(e))
                continue

        try:
            with open(tfile, 'r') as f:
                ucdata[tdom] = {}
                ucdata[tdom].update(default_conf)
                ucdata[tdom].update(yaml.load(f, Loader=CLoader))
            ucdata[tdom]['_mtime'] = int(ustat.st_mtime)
        except Exception as e:
            logger.error("Failed to load userconf for %s: %s", tdom, str(e))
            logger.warning("Reverting user config to defaults due to parsing errors: %s", tfile)
            create_default_userconf(user, tfile, tdat, profile=tprofile)
            try:
                with open(tfile, 'r') as f:
                    ucdata[tdom] = yaml.load(f, Loader=CLoader)
                ucdata[tdom]['_mtime'] = int(os.stat(tfile).st_mtime)
            except Exception as e:
                ucdata[tdom] = None
                logger.error("Default userconf creation failed %s: %s", tfile, str(e))
                continue

        # Check if user_default_override_local is enabled
        override_update = False
        if gconf.user_default_override_local and udefs:
            preup = json.dumps(ucdata[tdom])
            ucdata[tdom].update(udefs)
            postup = json.dumps(ucdata[tdom])
            if postup != preup:
                logger.info("Applied user overrides to %s; domain config updated", tdom)
                override_update = True
            else:
                logger.debug("User overrides already applied to %s; no change detected", tdom)

        # Set override to disable nginx cache
        if package_data.get('IMH_DISABLE_CACHE') == "1":
            if ucdata[tdom]['pass_all'] != True:
                logger.info("Applied 'disable cache' user overrides to %s; domain config updated", tdom)
                ucdata[tdom]['pass_all'] = True
                override_update = True
            else:
                logger.debug("User overrides 'disable cache' already applied to %s; no change detected", tdom)

        # Check if SSL options are up-to-date
        ssl_update = False
        if tdat.get('_ssl'):
            try:
                certpath = "%s/%s.pem" % (gconf.chained_cert_path, tdom.replace('-', '--').replace('.', '-'))
                mtime_cert = int(os.stat(certpath).st_mtime)
            except:
                mtime_cert = 0

            if force or tdat['_ssl']['_mtime'] > ucdata[tdom]['_mtime'] or \
               tdat['_ssl']['_mtime'] > mtime_cert:
                # Update SSL cert
                combpath = '/var/cpanel/ssl/apache_tls/%s/combined' % (tdat['servername'])
                if os.path.exists(combpath):
                    newcert, newkey = create_cert_from_combined(tdat['servername'], combpath)
                else:
                    newcert = create_chained_cert(tdat['servername'],
                                                  tdat['_ssl'].get('sslcertificatefile'),
                                                  tdat['_ssl'].get('sslcacertificatefile'))
                    newkey = tdat['_ssl'].get('sslcertificatekeyfile')

                if newcert is not None and newkey is not None:
                    ucdata[tdom]['ssl_enabled'] = True
                    ucdata[tdom]['ssl_certificate'] = newcert
                    ucdata[tdom]['ssl_certificate_key'] = newkey
                    ucdata[tdom]['proxy_proto'] = "https"
                    ssl_update = True
                else:
                    ucdata[tdom]['ssl_enabled'] = False
                    ucdata[tdom]['proxy_proto'] = "http"
                    ucdata[tdom]['ssl_certificate'] = None
                    ucdata[tdom]['ssl_certificate_key'] = None
                    logger.warning("Invalid SSL vhost defined for %s", user)
        else:
            if ucdata[tdom].get('ssl_enabled'):
                # Remove old chained cert
                try:
                    os.unlink(ucdata[tdom]['ssl_certificate'])
                    logger.info("Removed unused chained certificate: %s", ucdata[tdom]['ssl_certificate'])
                except Exception as e:
                    logger.warning("Failed to removed chained certificate %s: %s",
                                   ucdata[tdom]['ssl_certificate'], str(e))

                ucdata[tdom]['ssl_enabled'] = False
                ucdata[tdom]['proxy_proto'] = "http"
                ucdata[tdom]['ssl_certificate'] = None
                ucdata[tdom]['ssl_certificate_key'] = None
                ssl_update = True

        # Write updated userconf YAML file if SSL or override params have changed
        if ssl_update or override_update:
            if ssl_update:
                logger.info("Updated ssl_enabled status for %s -> %s", tdom, ucdata[tdom]['ssl_enabled'])

            try:
                with open(tfile, 'w') as f:
                    f.write(default_header.strip() + '\n')
                    yaml.dump(dict_strip_mtime(ucdata[tdom]), stream=f, Dumper=CDumper, default_flow_style=False)
                logger.debug("Updated SSL and/or override parameters in %s", tfile)
            except Exception as e:
                logger.error("Unable to write updated YAML file: %s", str(e))

            # Ensure correct file permissions
            try:
                os.chmod(tfile, 0o0664)
                os.chown(tfile, u_pwd.pw_uid, u_pwd.pw_gid)
            except Exception as e:
                logger.warning("Unable to set permissions/ownership of %s: %s", tfile, str(e))

            # Grab new mtime so that Nginx conf will be updated during the template rendering
            try:
                udata[tdom]['_mtime'] = int(os.stat(tfile).st_mtime)
            except Exception as e:
                logger.error("Failed to stat updated userconf %s: %s", tfile, str(e))

    logger.debug("Parsed userconf for %s", user)
    return ucdata

def create_default_userconf(user, ufile, udata, profile=None):
    """
    Create YAML file containing default user config
    """
    global udefs

    # Setup default base config based on chosen profile
    profile_name = profile
    cdata = None
    if profile is not None:
        cdata = get_profile(profile)

    if cdata is None:
        cdata = {}
        cdata.update(default_conf)
        profile_name = "(builtin_default)"

    if udefs:
        cdata.update(udefs)

    if udata.get('_ssl'):
        combpath = '/var/cpanel/ssl/apache_tls/%s/combined' % (udata['servername'])
        if os.path.exists(combpath):
            newcert, newkey = create_cert_from_combined(udata['servername'], combpath)
        else:
            newcert = create_chained_cert(udata['servername'],
                                          udata['_ssl'].get('sslcertificatefile'),
                                          udata['_ssl'].get('sslcacertificatefile'))
            newkey = udata['_ssl'].get('sslcertificatekeyfile')

        if newcert is not None and newkey is not None:
            cdata['ssl_enabled'] = True
            cdata['ssl_certificate'] = newcert
            cdata['ssl_certificate_key'] = newkey
            cdata['proxy_proto'] = "https"
        else:
            logger.warning("Invalid SSL vhost defined for %s", user)

    # Write userconf YAML file
    try:
        with open(ufile, 'w') as f:
            f.write(default_header.strip() + '\n')
            yaml.dump(dict_strip_mtime(cdata), stream=f, Dumper=CDumper, default_flow_style=False)
    except Exception as e:
        # On VZ containers root can't create, chown, or write to a user:user file if quota is exceeded for user
        # To work around this, remove the user:user file and recreate it as root:root (which ngxconf can read/write)
        if "Disk quota exceeded" in str(e):
            logger.error('Disk quota exceeded.  Unable to write to %s', ufile)
            os.unlink(ufile)
            with open(ufile, 'w') as f:
                f.write(default_header.strip() + '\n')
                yaml.dump(dict_strip_mtime(cdata), stream=f, Dumper=CDumper, default_flow_style=False)
            logger.debug("Wrote default configuration for %s from profile %s as root:root", user, profile_name)
            return True
            
        logger.error("Unable to write default userconf %s: %s", ufile, str(e))
        return False

    # Get user UID/GID
    try:
        u_pwd = pwd.getpwnam(user)
    except Exception as e:
        logger.error("Failed to translate username '%s' to UID/GID: %s", user, str(e))
        return None

    # Ensure correct file permissions
    try:
        os.chmod(ufile, 0o0664)
        os.chown(ufile, u_pwd.pw_uid, u_pwd.pw_gid)
    except Exception as e:
        logger.warning("Unable to set permissions/ownership of %s: %s", ufile, str(e))

    logger.debug("Wrote default configuration for %s from profile %s", user, profile_name)
    return True

def create_cert_from_combined(domain, cpath):
    """
    Create a chained certificate from cPanel combined TLS data
    """
    chained = "%s/%s.pem" % (gconf.chained_cert_path, domain.replace('-', '--').replace('.', '-'))
    keypath = "%s/%s.key" % (gconf.chained_cert_path, domain.replace('-', '--').replace('.', '-'))
    ckey = ""
    certdata = ""

    # Parse combined PEM certificate
    try:
        clist = pem.parse_file(cpath)
    except Exception as e:
        logger.error("Failed to parse combined cert for %s at %s: %s", domain, cpath, str(e))
        return (None, None)

    for tpem in clist:
        if isinstance(tpem, pem.Key):
            ckey = str(tpem)
        elif isinstance(tpem, pem.Certificate):
            certdata += str(tpem)
        else:
            logger.warning("Found unexpected PEM component of type <%s> in %s", type(tpem), cpath)

    if not len(ckey) or not len(certdata):
        logger.error("Missing certificate and/or keys from combined TLS store at %s", cpath)
        return (None, None)

    # Write chained cert
    try:
        with open(chained, 'w') as f:
            f.write(certdata)
        logger.debug("Wrote chained certificate from cP 68+ combined format: %s", chained)
    except Exception as e:
        logger.error("Failed to write chained certificate for %s: %s", domain, str(e))
        return (None, None)

    # Write key
    try:
        with open(keypath, 'w') as f:
            f.write(ckey)
        os.chmod(keypath, 0o0600)
        logger.debug("Wrote keyfile from cP 68+ combined format: %s", keypath)
    except Exception as e:
        logger.error("Failed to write keyfile for %s: %s", domain, str(e))
        return (None, None)
    return (chained, keypath)

def create_chained_cert(domain, certpath, capath, outfile=None):
    """
    Created a chained certificate consisting of the certificate + CA bundle
    """
    if outfile is None:
        chained = "%s/%s.pem" % (gconf.chained_cert_path, domain.replace('-', '--').replace('.', '-'))
    else:
        chained = outfile

    # Read certificate
    try:
        with open(certpath, 'r') as f:
            certdata = f.read().strip()
    except Exception as e:
        logger.error("Failed to read certificate file for %s: %s", domain, str(e))
        return None

    # Read intermediates (cabundle)
    if capath is not None:
        try:
            with open(capath, 'r') as f:
                cadata = f.read().strip()
        except Exception as e:
            logger.error("Failed to read CA bundle for %s: %s", domain, str(e))
            cadata = ""
    else:
        logger.warning("No CA bundle defined for %s", domain)
        cadata = ""

    # Write chained cert
    try:
        with open(chained, 'w') as f:
            f.write(certdata + '\n' + cadata)
        logger.debug("Wrote chained certificate: %s", chained)
    except Exception as e:
        logger.error("Failed to write chained certificate for %s: %s", domain, str(e))
        return None
    return chained

def parse_userdata(user):
    """
    Parse cPanel userdata for @user
    """
    main = parse_userdata_file(user, 'main')
    if main is None:
        logger.warning("Abort parsing userdata for user '%s'", user)
        return None

    domlist = [main['main_domain']] + main['sub_domains']
    udata = {'_main': main, '_suspended': check_suspended(user)}

    for tdom in domlist:
        tdata = parse_userdata_file(user, tdom)
        tssl = parse_userdata_file(user, tdom, check_ssl=True)
        if tdata is None:
            continue
        if tssl:
            try:
                tdata['_ssl'] = tssl
            except Exception as e:
                logger.error("Failed to extract SSL information for %s:%s - %s", user, tdom, str(e))
                tdata['_ssl'] = None
        else:
            tdata['_ssl'] = None

        udata[tdom] = {**tdata, '_suspended': udata['_suspended']}
    return udata

def parse_userdata_file(user, ufile, check_ssl=False, prefix='/var/cpanel/userdata'):
    """
    Parse single cPanel userdata file; injects [_mtime] field from stat result
    """
    datfile = "%s/%s/%s" % (prefix, user, ufile)
    if check_ssl:
        datfile += "_SSL"
        try:
            os.stat(datfile)
        except:
            return None
    try:
        with open(datfile, 'r') as f:
            ddata = yaml.load(f, Loader=CLoader)
        ddata['_mtime'] = int(os.stat(datfile).st_mtime)
        return ddata
    except Exception as e:
        logger.error("Failed to parse userdata file %s: %s", datfile, str(e))
        return None

def check_suspended(user):
    """
    Check if @user is suspended; if so, return a normalized reason string,
    otherwise, return None
    """
    suspath = "/var/cpanel/suspended/%s" % (user)
    try:
        os.stat(suspath)
        with open(suspath, 'r') as f:
            rreason = f.read().strip()
    except:
        return None

    try:
        rd = re.match(r'^((?:(?P<reasonA>[a-z]+):(?P<detail>.+))|(?:\[PP2 Auto\] - Reason: (?P<reasonB>.+)))$',
                      rreason).groupdict()
        return (rd['reasonA'] if rd['reasonA'] else rd['reasonB']).strip().lower()
    except:
        logger.warning("Failed to parse suspension reason from %s", suspath)
        return 'unknown'

def get_last_nconf_updates(user, basedir='/etc/nginx/vhosts'):
    """
    Return a dict of mtime (int) for each nginx config file
    """
    nconf = {}
    for tpath in glob("%s/%s__*.conf" % (basedir, user)):
        # extract domain from filename
        tfile = os.path.basename(tpath)
        try:
            tdom = tfile.replace('.conf', '').split('__', 1)[1].replace('_', '.')
        except:
            logger.error("Failed to parse domain from vhost config: %s", tfile)
            continue

        # grab the mtime
        try:
            nconf[tdom] = int(os.stat(tpath).st_mtime)
        except Exception as e:
            logger.error("Failed to stat vhost config %s: %s", tpath, str(e))
            continue
    return nconf

def check_lock(lockfile='/var/run/ngxconf.pid', kill_previous=False):
    """
    Check if lockfile exists; if not, create one
    If lockfile DOES exist and @kill_previous is True,
    kills the previous ngxconf runner and create a new lockfile
    """
    try:
        os.stat(lockfile)
        with open(lockfile, 'r') as f:
            old_pid = int(f.read())
        os.kill(old_pid, 0)
        logger.warning("ngxconf is already running with PID %d", old_pid)
        if kill_previous:
            logger.warning("Halting running instance with SIGUSR1. This ngxconf will take over.")
            os.kill(old_pid, signal.SIGUSR1)
        else:
            logger.error("Aborting due to ngxconf already running (%d)", old_pid)
            sys.exit(10)
    except:
        pass

    # Write new lock file with current PID
    tpid = str(os.getpid())
    with open(lockfile, 'w') as f:
        f.write(tpid)
    logger.debug("Wrote lock file %s (PID %s)", lockfile, tpid)

def clear_lock(lockfile='/var/run/ngxconf.pid'):
    """
    Remove lock/pidfile
    """
    try:
        os.unlink(lockfile)
        logger.debug("Lock file removed: %s", lockfile)
        return True
    except Exception as e:
        logger.warning("Failed to remove lockfile %s: %s", lockfile, str(e))
        return False

def sigusr_handler(sig, frame):  # pylint: disable=unused-argument
    """
    Signal handler for SIGUSR1
    """
    logger.error("Received signal from another running ngxconf. Aborting.")
    sys.exit(10)

def sigint_handler(sig, frame):  # pylint: disable=unused-argument
    """
    Signal handler for SIGINT (interrupt)
    """
    logger.error("Interrupted.")
    clear_lock()
    sys.exit(11)

Zerion Mini Shell 1.0