PNG  IHDRxsBIT|d pHYs+tEXtSoftwarewww.inkscape.org<,tEXtComment File Manager

File Manager

Path: /opt/cloudlinux/venv/lib/python3.11/site-packages/

Viewing File: cldetectlib.py

# -*- coding: utf-8 -*-

# CLDETECT python lib

#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

# Detection:
#
# Control Panel name & version
# Control Panel name
# Control Panel admin email
# CXS is installed
# mod_suphp is enabled for easyapache on cPanel
# get apache gid
# Detect LiteSpeed
# Detect PostGreSQL
# Detect admin user for DirectAdmin control panel
# Detect CloudLinux instalation process
# Detect Nagios
# Detect if cloudlinux=yes is present for DirectAdmin
# Get fs.enforce_symlinksifowner from /etc/sysctl.conf
# Detect suEXEC
# Detect suPHP
# Check suEXEC or suPHP for SecureLVE jail
# Check /etc/ssh/sshd_config for UsePAM yes
# Separate functions for detect machines: is_da, is_isp, etc
# Detect cagefs installed

import os
import pwd
import re
import subprocess
import sys
from configparser import ConfigParser, NoOptionError, NoSectionError

from clcommon import cpapi
from clcommon.sysctl import SYSCTL_CL_CONF_FILE, SysCtlConf

# Control panel name
CP_NAME = None
# Control panel version
CP_VERSION = None
# If CP_NAME is "ISPManager" and CP_VERSION is "5.xx" ISP5 Type: "Master" or "Node".
# else - always None
CP_ISP_TYPE = None
CP_ADMIN_EMAIL = None
NAGIOS_GID = 0
APACHE_GID = 48
APACHE_UNAME = "apache"
LITESPEED_CONFIG_FILE = "/usr/local/lsws/conf/httpd_config.xml"
LITESPEED_OPEN_CONFIG_FILE = "/usr/local/lsws/conf/httpd_config.conf"
LITESPEED_VERSION_FILE = "/usr/local/lsws/VERSION"
POSTGRE_SERVER_FILE = None
POSTGRE_SYSTEMD_PATH = "/usr/lib/systemd/system/postgresql.service"
POSTGRE_INITD_PATH = "/etc/rc.d/init.d/postgresql"
CL_SETUP_LOCK_FILE = "/var/lock/cldeploy.lck"
CL_CONFIG_FILE = "/etc/sysconfig/cloudlinux"
USEPAM_FILE = "/etc/ssh/sshd_config"
SUEXEC_ENABLED = None
SUPHP_ENABLED = None

SHARED_PRO_EDITION_HUMAN_READABLE = "CloudLinux OS Shared Pro"
SHARED_EDITION_HUMAN_READABLE = "CloudLinux OS Shared"
SOLO_EDITION_HUMAN_READABLE = "CloudLinux OS Solo"


if os.path.isfile(POSTGRE_SYSTEMD_PATH):
    POSTGRE_SERVER_FILE = POSTGRE_SYSTEMD_PATH
else:
    POSTGRE_SERVER_FILE = POSTGRE_INITD_PATH


def is_ea4():
    return os.path.exists("/etc/cpanel/ea4/is_ea4")


# This function get CP name and CP version
def getCP():
    global CP_NAME
    global CP_VERSION
    global CP_ISP_TYPE

    CP_NAME = "Unknown"
    CP_VERSION = "0"
    CP_ISP_TYPE = None

    ####################################################################
    # Try to detect panels supported by CL and custom panel with cpapi plugin
    try:
        panel_data = cpapi.get_cp_description()
        if panel_data:
            CP_NAME = panel_data["name"]
            CP_VERSION = panel_data["version"]
            CP_ISP_TYPE = panel_data["additional_info"]
    except Exception:
        pass

    # Try to detect some other panels without retrieving info about them
    ####################################################################
    # H-Sphere
    try:
        with open("/hsphere/shared/version", encoding="utf-8") as f:
            data = f.read()
            release = re.findall(r"Release:\s+(.+)", data)[0]
            version = re.findall(r"Version:\s+(.+)", data)[0]
            CP_NAME = "H-Sphere"
            CP_VERSION = f"{release}.{version}"
            return True
    except Exception:
        pass

    ####################################################################
    # HostingNG check
    if os.path.isfile("/lib64/libnss_ng.so"):
        CP_NAME = "HostingNG"
        CP_VERSION = "none"
        return True

    ####################################################################
    # CentOS Web Panel check
    if os.path.isdir("/usr/local/cwpsrv"):
        CP_NAME = "CentOS_WEB_Panel"
        CP_VERSION = "none"
        return True

    # Atomia check: (what is atomia you can see at www.atomia.com)
    # Atomia is more than just CP inside the CloudLinux,
    # So we just check presence of Atomia program agent
    # by its footprints - config files, which agent created.
    if os.path.isfile("/etc/httpd/conf.d/atomia-pa-apache.conf") or os.path.isdir("/storage/configuration/cloudlinux"):
        CP_NAME = "Atomia_agent"
        CP_VERSION = "none"
        return True

    # Cyber Panel
    if os.path.isdir("/usr/local/CyberCP"):
        CP_NAME = "Cyberpanel"
        CP_VERSION = "none"
        return True

    # Planet Hoster
    if os.path.isdir("/var/phmgr"):
        CP_NAME = "PlaneHoster"
        CP_VERSION = "none"
        return True

    # Vesta CP, check it`s main dir
    # can install from https://vestacp.com/install/
    if os.path.isdir("/usr/local/vesta"):
        CP_NAME = "Vesta"
        CP_VERSION = "none"
        return True

    # We can check if VirtualminWebmin is installed by checking the license file.
    # That file is always present, license serial and key are predefined
    # in the beginning of the installation script
    if os.path.isfile("/etc/virtualmin-license"):
        CP_NAME = "VirtualminWebmin"
        CP_VERSION = "none"
        return True

    # Detect Webuzo panel
    if os.path.isfile("/usr/local/webuzo/universal.php"):
        CP_NAME = "Webuzo"
        CP_VERSION = "none"
        return True

    # No panel detected
    return False


# Get params value from file
def get_param_from_file(file_name, param_name, separator=None, default_val=""):
    try:
        with open(file_name, encoding="utf-8") as f:
            content = f.readlines()
    except OSError:
        return default_val
    for line in content:
        line = line.strip()
        if line.startswith(param_name):
            lineParts = line.split(separator)
            if (len(lineParts) == 2) and (lineParts[0].strip() == param_name):
                return lineParts[1].strip()
    return default_val


# This function get CP name only
def getCPName():
    global CP_NAME
    if CP_NAME:
        return CP_NAME

    # cPanel check
    if os.path.isfile("/usr/local/cpanel/cpanel"):
        CP_NAME = "cPanel"

    # Plesk check
    elif os.path.isfile("/usr/local/psa/version"):
        CP_NAME = "Plesk"

    # DirectAdmin check
    elif os.path.isfile("/usr/local/directadmin/directadmin"):
        CP_NAME = "DirectAdmin"

    # ISPmanager v4 or v5 check
    elif os.path.isfile("/usr/local/ispmgr/bin/ispmgr") or os.path.isdir("/usr/local/mgr5"):
        CP_NAME = "ISPManager"

    # InterWorx check
    elif os.path.isdir("/usr/local/interworx"):
        CP_NAME = "InterWorx"

    # HSphere check
    elif os.path.isdir("/hsphere/shared"):
        CP_NAME = "H-Sphere"
    elif os.path.isfile("/lib64/libnss_ng.so"):
        CP_NAME = "HostingNG"

    # CentOS Web Panel check
    elif os.path.isdir("/usr/local/cwpsrv"):
        CP_NAME = "CentOS_WEB_Panel"

    elif os.path.isfile("/etc/httpd/conf.d/atomia-pa-apache.conf") or os.path.isdir(
        "/storage/configuration/cloudlinux"
    ):
        CP_NAME = "Atomia_agent"

    elif os.path.isdir("/usr/local/vesta"):
        CP_NAME = "Vesta"

    elif os.path.isfile("/etc/virtualmin-license"):
        CP_NAME = "VirtualminWebmin"

    elif os.path.isdir("/var/phmgr"):
        CP_NAME = "PlaneHoster"

    elif os.path.isdir("/usr/local/CyberCP"):
        CP_NAME = "Cyberpanel"

    elif os.path.isfile("/usr/local/webuzo/universal.php"):
        CP_NAME = "Webuzo"

    else:
        # Detect custom panel name
        panel_data = cpapi.get_cp_description()
        # If panel data retreived, use its name
        CP_NAME = panel_data["name"] if panel_data else "Unknown"

    return CP_NAME


def add_server_stats(status_report):
    """
    Add server statistics to status_report dict
    :param status_report: dict to add statistics to
    :type status_report: dict
    """
    from clcommon import ClPwd  # pylint: disable=import-outside-toplevel

    res = {}
    cp_name = getCPName()
    if cp_name != "Unknown":
        res["cp"] = cp_name

    if cp_name == "Plesk":
        clpwd = ClPwd(10000)
    else:
        clpwd = ClPwd()

    d = clpwd.get_uid_dict()
    users = 0
    sys_users = {
        "nfsnobody",
        "avahi-autoipd",
        "exim",
        "clamav",
        "varnish",
        "nagios",
        "saslauth",
        "mysql",
        "lsadm",
        "systemd-bus-proxy",
        "systemd-network",
        "polkitd",
        "firebird",
        "nginx",
        "dovecot",
        "dovenull",
        "roundcube_sysuser",
        "cpanel",
        "cpanelhorde",
        "cpanelphpmyadmin",
        "cpanelphppgadmin",
        "cpanelroundcube",
        "mailman",
        "cpaneleximfilter",
        "cpanellogaholic",
        "cpanellogin",
        "munin",
        "cpaneleximscanner",
        "cpanelphpgadmin",
        "cpses",
        "cpanelconnecttrack",
        "cpanelrrdtool",
        "admin",
        "webapps",
        "apache",
        "diradmin",
        "majordomo",
        "viapm",
        "iworx",
        "iworx-web",
        "iworx-pma",
        "iworx-backup",
        "iworx-horde",
        "iworx-roundcube",
        "iworx-sqmail",
        "iworx_support_user",
        "psaadm",
        "popuser",
        "psaftp",
        "drweb",
        "sw-cp-server",
        "horde_sysuser",
    }
    for pw_entries in d.values():
        found = False
        for entry in pw_entries:
            if entry.pw_name in sys_users:
                found = True
                break
        if not found:
            users += 1
    res["users"] = users
    status_report["cln"] = res


# Control Panel admin email
def getCPAdminEmail():
    global CP_ADMIN_EMAIL
    if CP_ADMIN_EMAIL:
        return CP_ADMIN_EMAIL

    if not os.path.isfile(CL_CONFIG_FILE):
        print("Error: missing " + CL_CONFIG_FILE + " config file.")
        sys.exit(1)
    try:
        parser = ConfigParser(interpolation=None, strict=False)
        parser.read(CL_CONFIG_FILE)
        if parser.get("license_check", "EMAIL").strip().find("@") != -1:
            CP_ADMIN_EMAIL = parser.get("license_check", "EMAIL").strip()
        else:
            try:
                getCPName()
                get_email_script = parser.get("license_check", CP_NAME + "_getemail_script")
                if not os.path.isfile(get_email_script):
                    raise FileNotFoundError
                with subprocess.Popen(
                    [get_email_script],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True,
                ) as proc:
                    out, _ = proc.communicate()
                CP_ADMIN_EMAIL = out.strip()
            except (NoSectionError, NoOptionError, FileNotFoundError):
                CP_ADMIN_EMAIL = "root@localhost.localdomain"
        return CP_ADMIN_EMAIL
    except Exception:
        print("Error: bad " + CL_CONFIG_FILE + " config file.")
        sys.exit(1)


# Check is CXS installed
def CXS_check():
    return os.path.isdir("/etc/cxs")


# Check is mod_suphp is enabled in easyapache on cPanel
# TODO check cagefs_posteasyapache_hook.sh for suPHP check via /usr/local/cpanel/bin/rebuild_phpconf --available
def mod_suPHP_check():
    getCPName()
    if CP_NAME != "cPanel":
        return False

    return os.path.isfile("/usr/local/apache/modules/mod_suphp.so")


# Get Apache gid
def get_apache_gid():
    getCPName()
    global APACHE_GID
    global APACHE_UNAME
    if CP_VERSION == "0":
        return False

    if CP_NAME == "cPanel":
        APACHE_UNAME = "nobody"

    if CP_NAME == "H-Sphere":
        APACHE_UNAME = "httpd"

    # line 24 | APACHE_UNAME = 'apache' - for others control panel (DA,ISP,IWorx,Plesk)

    try:
        APACHE_GID = pwd.getpwnam(APACHE_UNAME).pw_gid
    except Exception:
        pass
    return True


# Detect LiteSpeed
def detect_litespeed():
    """
    LiteSpeed can be enterprise or open source, and each of them
    stores config in different formats
    So this checker will search for one of them
    """
    return detect_enterprise_litespeed() or detect_open_litespeed()


def detect_enterprise_litespeed():
    """
    Detect LSWS Enterprise presence
    """
    return os.path.isfile(LITESPEED_CONFIG_FILE)


def detect_open_litespeed():
    """
    Detect OpenLiteSpeed presence
    """
    return os.path.isfile(LITESPEED_OPEN_CONFIG_FILE)


def get_litespeed_version():
    """
    Determine Litespeed version.
    Works for both LSWS Enterprise and OpenLiteSpeed.
    """
    try:
        # Content of LITESPEED_VERSION_FILE: '5.4.12'
        with open(LITESPEED_VERSION_FILE, encoding="utf-8") as f:
            return f.read().strip()
    except (FileNotFoundError, OSError):
        return ""


# Detect PostGreSQL
def detect_postgresql():
    return os.path.isfile(POSTGRE_SERVER_FILE)


# Detect DirectAdmin admin user
def detect_DA_admin():
    getCPName()
    if CP_NAME != "DirectAdmin":
        return False

    try:
        with open("/usr/local/directadmin/conf/directadmin.conf", encoding="utf-8") as f:
            out = f.read()
        return out.split("admindir=")[1].split("\n")[0].split("/")[-1].strip()
    except Exception:
        return "admin"


# Detect CloudLinux instalation process
def check_CL_installing():
    if not os.path.isfile(CL_SETUP_LOCK_FILE):
        return False

    try:
        with open(CL_SETUP_LOCK_FILE, encoding="utf-8") as f:
            pid = int(f.read())
        return os.path.isdir(f"/proc/{pid}")
    except Exception:
        return False


# Detect Nagios
def get_nagios():
    if not os.path.isdir("/usr/local/nagios"):
        return False

    global NAGIOS_GID
    try:
        NAGIOS_GID = pwd.getpwnam("nagios").pw_gid
        return True
    except Exception:
        return False


# Detect if cloudlinux=yes is present for DirectAdmin
def da_check_options():
    check_result = get_param_from_file("/usr/local/directadmin/custombuild/options.conf", "cloudlinux", "=")
    return check_result == "yes"


def get_symlinksifowner():
    """get fs.enforce_symlinksifowner from sysctl conf"""
    sysctl = SysCtlConf(config_file=SYSCTL_CL_CONF_FILE, mute_errors=False)
    value = sysctl.get("fs.enforce_symlinksifowner")
    return int(value) if value is not None else value


# Get suEXEC status
def get_suEXEC_status():
    global SUEXEC_ENABLED
    if SUEXEC_ENABLED is None:
        detect_suEXEC_suPHP()
    return SUEXEC_ENABLED


# Get suPHP status():
def get_suPHP_status():
    global SUPHP_ENABLED
    if SUPHP_ENABLED is None:
        detect_suEXEC_suPHP()
    return SUPHP_ENABLED


# Detect suEXEC  and suPHP
def detect_suEXEC_suPHP():
    global SUEXEC_ENABLED
    global SUPHP_ENABLED

    # This helps us to avoid double check when we checks both suEXEC and suPHP
    SUEXEC_ENABLED = False
    SUPHP_ENABLED = False

    modules = get_apache_modules()
    if modules is None:
        return
    SUEXEC_ENABLED = "suexec_module" in modules
    SUPHP_ENABLED = "suphp_module" in modules


def get_apache_modules():
    #  path to httpd is the same on the panels
    bin_exec = "/usr/sbin/httpd"
    try:
        with subprocess.Popen(
            [bin_exec, "-M"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        ) as proc:
            out, _ = proc.communicate()
        modules = []
        out = out.split("\n")
        #  clean the output from 1st line 'Loaded modules'
        for line in out[1:]:
            if not line:
                continue
            # core_module (static) so_module (static) http_module (static) mpm_worker_module (shared)...
            #  --> ['core_module', 'so_module', 'http_module', 'mpm_worker_module']
            try:
                mod = line.strip().split(" ")[0]
            except IndexError:
                mod = ""
            if mod == "":
                continue
            modules.append(mod)
    except OSError:
        return None
    return modules


def execute(command):
    """
    Execute command with bash interpreter
    """
    with subprocess.Popen(
        command,
        shell=True,
        executable="/bin/bash",
        stdout=subprocess.PIPE,
        text=True,
        bufsize=-1,
    ) as proc:
        return proc.communicate()[0]


# check suPHP or suEXEC binary for jail
def check_binary_has_jail(location):
    try:
        if is_ea4():
            result = execute("/usr/bin/strings " + str(location[getCPName() + "_ea4"]) + " | grep jail")
        else:
            result = execute("/usr/bin/strings " + str(location[getCPName()]) + " | grep jail")

        return result.find("jail error") != -1
    except KeyError:
        return None
    except OSError:
        return False


# Check sshd -T output for usepam yes
def check_SSHd_UsePAM():
    try:
        result = execute("/usr/sbin/sshd -T | grep usepam")
        return result.find("usepam yes") != -1
    except OSError:
        return None


def init_cp_name():
    if CP_NAME is None:
        getCPName()


# NOTE: This section of code is deprecated and should not be added to.


# Detect DirectAdmin machine
def is_da():
    init_cp_name()
    return CP_NAME == "DirectAdmin"


# Detect ISP Manager machine
def is_ispmanager():
    init_cp_name()
    return CP_NAME == "ISPManager"


# Detect ISP Manager v5 machine type: "Master" or "Node"
# If not ISP5 - always None
def ispmanager5_type():
    init_cp_name()
    return CP_ISP_TYPE


# Detect ISP Manager v5 machine is Master
def ispmanager5_is_master():
    return CP_ISP_TYPE == "Master"


# Detect cPanel machine
def is_cpanel():
    init_cp_name()
    return CP_NAME == "cPanel"


# Detect Plesk machine
def is_plesk():
    init_cp_name()
    return CP_NAME == "Plesk"


# Detect InterWorx machine
def is_internetworx():
    init_cp_name()
    return CP_NAME == "InterWorx"


# Detect H-Sphere machine
def is_hsphere():
    init_cp_name()
    return CP_NAME == "H-Sphere"


# Detect HostingNG machine
def is_hostingng():
    init_cp_name()
    return CP_NAME == "HostingNG"


# Detect unknown machine
def is_unknown():
    init_cp_name()
    return CP_NAME == "Unknown"


def is_openvz():
    """
    Return 0 if there is no OpenVZ, otherwise return node ID (envID)
    """
    pid = os.getpid()
    with open(f"/proc/{pid}/status", encoding="utf-8") as f:
        for line in f:
            if line.startswith("envID:"):
                env_id = line.split(":")[1].strip()
                return int(env_id)
    return 0  # no openvz found


def is_cagefs_installed():
    return os.path.exists("/usr/sbin/cagefsctl")


def get_boolean_param(file_name, param_name, separator="=", default_val=True):
    config_val = get_param_from_file(file_name, param_name, separator, default_val=None)
    if config_val is None:
        return default_val
    return config_val.lower() in ("true", "1", "yes", "on")
b IDATxytVսϓ22 A@IR :hCiZ[v*E:WũZA ^dQeQ @ !jZ'>gsV仿$|?g)&x-EIENT ;@xT.i%-X}SvS5.r/UHz^_$-W"w)Ɗ/@Z &IoX P$K}JzX:;` &, ŋui,e6mX ԵrKb1ԗ)DADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADADA݀!I*]R;I2$eZ#ORZSrr6mteffu*((Pu'v{DIߔ4^pIm'77WEEE;vƎ4-$]'RI{\I&G :IHJ DWBB=\WR޽m o$K(V9ABB.}jѢv`^?IOȅ} ڶmG}T#FJ`56$-ھ}FI&v;0(h;Б38CӧOWf!;A i:F_m9s&|q%=#wZprrrla A &P\\СC[A#! {olF} `E2}MK/vV)i{4BffV\|ۭX`b@kɶ@%i$K z5zhmX[IXZ` 'b%$r5M4º/l ԃߖxhʔ)[@=} K6IM}^5k㏷݆z ΗÿO:gdGBmyT/@+Vɶ纽z񕏵l.y޴it뭷zV0[Y^>Wsqs}\/@$(T7f.InݺiR$푔n.~?H))\ZRW'Mo~v Ov6oԃxz! S,&xm/yɞԟ?'uaSѽb,8GלKboi&3t7Y,)JJ c[nzӳdE&KsZLӄ I?@&%ӟ۶mSMMњ0iؐSZ,|J+N ~,0A0!5%Q-YQQa3}$_vVrf9f?S8`zDADADADADADADADADAdqP,تmMmg1V?rSI꒟]u|l RCyEf٢9 jURbztѰ!m5~tGj2DhG*{H9)꒟ר3:(+3\?/;TUݭʴ~S6lڧUJ*i$d(#=Yݺd{,p|3B))q:vN0Y.jkק6;SɶVzHJJЀ-utѹսk>QUU\޲~]fFnK?&ߡ5b=z9)^|u_k-[y%ZNU6 7Mi:]ۦtk[n X(e6Bb."8cۭ|~teuuw|ήI-5"~Uk;ZicEmN/:]M> cQ^uiƞ??Ңpc#TUU3UakNwA`:Y_V-8.KKfRitv޲* 9S6ֿj,ՃNOMߤ]z^fOh|<>@Å5 _/Iu?{SY4hK/2]4%it5q]GGe2%iR| W&f*^]??vq[LgE_3f}Fxu~}qd-ږFxu~I N>\;͗O֊:̗WJ@BhW=y|GgwܷH_NY?)Tdi'?խwhlmQi !SUUsw4kӺe4rfxu-[nHtMFj}H_u~w>)oV}(T'ebʒv3_[+vn@Ȭ\S}ot}w=kHFnxg S 0eޢm~l}uqZfFoZuuEg `zt~? b;t%>WTkķh[2eG8LIWx,^\thrl^Ϊ{=dž<}qV@ ⠨Wy^LF_>0UkDuʫuCs$)Iv:IK;6ֲ4{^6եm+l3>݆uM 9u?>Zc }g~qhKwڭeFMM~pМuqǿz6Tb@8@Y|jx](^]gf}M"tG -w.@vOqh~/HII`S[l.6nØXL9vUcOoB\xoǤ'T&IǍQw_wpv[kmO{w~>#=P1Pɞa-we:iǏlHo׈꒟f9SzH?+shk%Fs:qVhqY`jvO'ρ?PyX3lх]˾uV{ݞ]1,MzYNW~̈́ joYn}ȚF߾׮mS]F z+EDxm/d{F{-W-4wY듏:??_gPf ^3ecg ҵs8R2מz@TANGj)}CNi/R~}c:5{!ZHӋӾ6}T]G]7W6^n 9*,YqOZj:P?Q DFL|?-^.Ɵ7}fFh׶xe2Pscz1&5\cn[=Vn[ĶE鎀uˌd3GII k;lNmشOuuRVfBE]ۣeӶu :X-[(er4~LHi6:Ѻ@ԅrST0trk%$Č0ez" *z"T/X9|8.C5Feg}CQ%͞ˣJvL/?j^h&9xF`њZ(&yF&Iݻfg#W;3^{Wo^4'vV[[K';+mӍִ]AC@W?1^{එyh +^]fm~iԵ]AB@WTk̏t uR?l.OIHiYyԶ]Aˀ7c:q}ힽaf6Z~қm(+sK4{^6}T*UUu]n.:kx{:2 _m=sAߤU@?Z-Vކеz왍Nэ{|5 pڶn b p-@sPg]0G7fy-M{GCF'%{4`=$-Ge\ eU:m+Zt'WjO!OAF@ik&t݆ϥ_ e}=]"Wz_.͜E3leWFih|t-wZۍ-uw=6YN{6|} |*={Ѽn.S.z1zjۻTH]흾 DuDvmvK.`V]yY~sI@t?/ϓ. m&["+P?MzovVЫG3-GRR[(!!\_,^%?v@ҵő m`Y)tem8GMx.))A]Y i`ViW`?^~!S#^+ѽGZj?Vģ0.))A꨷lzL*]OXrY`DBBLOj{-MH'ii-ϰ ok7^ )쭡b]UXSְmռY|5*cֽk0B7镹%ڽP#8nȎq}mJr23_>lE5$iwui+ H~F`IjƵ@q \ @#qG0".0" l`„.0! ,AQHN6qzkKJ#o;`Xv2>,tێJJ7Z/*A .@fفjMzkg @TvZH3Zxu6Ra'%O?/dQ5xYkU]Rֽkق@DaS^RSּ5|BeHNN͘p HvcYcC5:y #`οb;z2.!kr}gUWkyZn=f Pvsn3p~;4p˚=ē~NmI] ¾ 0lH[_L hsh_ғߤc_њec)g7VIZ5yrgk̞W#IjӪv>՞y睝M8[|]\շ8M6%|@PZڨI-m>=k='aiRo-x?>Q.}`Ȏ:Wsmu u > .@,&;+!!˱tﭧDQwRW\vF\~Q7>spYw$%A~;~}6¾ g&if_=j,v+UL1(tWake:@Ș>j$Gq2t7S?vL|]u/ .(0E6Mk6hiۺzښOrifޱxm/Gx> Lal%%~{lBsR4*}{0Z/tNIɚpV^#Lf:u@k#RSu =S^ZyuR/.@n&΃z~B=0eg뺆#,Þ[B/?H uUf7y Wy}Bwegל`Wh(||`l`.;Ws?V@"c:iɍL֯PGv6zctM̠':wuW;d=;EveD}9J@B(0iհ bvP1{\P&G7D޴Iy_$-Qjm~Yrr&]CDv%bh|Yzni_ˆR;kg}nJOIIwyuL}{ЌNj}:+3Y?:WJ/N+Rzd=hb;dj͒suݔ@NKMԄ jqzC5@y°hL m;*5ezᕏ=ep XL n?מ:r`۵tŤZ|1v`V뽧_csج'ߤ%oTuumk%%%h)uy]Nk[n 'b2 l.=͜E%gf$[c;s:V-͞WߤWh-j7]4=F-X]>ZLSi[Y*We;Zan(ӇW|e(HNNP5[= r4tP &0<pc#`vTNV GFqvTi*Tyam$ߏWyE*VJKMTfFw>'$-ؽ.Ho.8c"@DADADADADADADADADA~j*֘,N;Pi3599h=goضLgiJ5փy~}&Zd9p֚ e:|hL``b/d9p? fgg+%%hMgXosج, ΩOl0Zh=xdjLmhݻoO[g_l,8a]٭+ӧ0$I]c]:粹:Teꢢ"5a^Kgh,&= =՟^߶“ߢE ܹS J}I%:8 IDAT~,9/ʃPW'Mo}zNƍ쨓zPbNZ~^z=4mswg;5 Y~SVMRXUյڱRf?s:w ;6H:ºi5-maM&O3;1IKeamZh͛7+##v+c ~u~ca]GnF'ټL~PPPbn voC4R,ӟgg %hq}@#M4IÇ Oy^xMZx ) yOw@HkN˖-Sǎmb]X@n+i͖!++K3gd\$mt$^YfJ\8PRF)77Wא!Cl$i:@@_oG I{$# 8磌ŋ91A (Im7֭>}ߴJq7ޗt^ -[ԩSj*}%]&' -ɓ'ꫯVzzvB#;a 7@GxI{j޼ƌ.LÇWBB7`O"I$/@R @eee@۷>}0,ɒ2$53Xs|cS~rpTYYY} kHc %&k.], @ADADADADADADADADA@lT<%''*Lo^={رc5h %$+CnܸQ3fҥK}vUVVs9G R,_{xˇ3o߾;TTTd}馛]uuuG~iԩ@4bnvmvfϞ /Peeeq}}za I~,誫{UWW뮻}_~YƍSMMMYχ֝waw\ďcxꩧtEƍկ_?۷5@u?1kNׯWzz/wy>}zj3 k(ٺuq_Zvf̘:~ ABQ&r|!%KҥKgԞ={<_X-z !CyFUUz~ ABQIIIjݺW$UXXDٳZ~ ABQƍecW$<(~<RSSvZujjjԧOZQu@4 8m&&&jԩg$ď1h ͟?_{768@g =@`)))5o6m3)ѣƌJ;wҿUTT /KZR{~a=@0o<*狔iFɶ[ˎ;T]]OX@?K.ۈxN pppppppppppppppppPfl߾] ,{ァk۶mڿo5BTӦMӴiӴ|r DB2e|An!Dy'tkΝ[A $***t5' "!駟oaDnΝ:t֭[gDШQ06qD;@ x M6v(PiizmZ4ew"@̴ixf [~-Fٱc&IZ2|n!?$@{[HTɏ#@hȎI# _m(F /6Z3z'\r,r!;w2Z3j=~GY7"I$iI.p_"?pN`y DD?: _  Gÿab7J !Bx@0 Bo cG@`1C[@0G @`0C_u V1 aCX>W ` | `!<S `"<. `#c`?cAC4 ?c p#~@0?:08&_MQ1J h#?/`7;I  q 7a wQ A 1 Hp !#<8/#@1Ul7=S=K.4Z?E_$i@!1!E4?`P_  @Bă10#: "aU,xbFY1 [n|n #'vEH:`xb #vD4Y hi.i&EΖv#O H4IŶ}:Ikh @tZRF#(tXҙzZ ?I3l7q@õ|ۍ1,GpuY Ꮿ@hJv#xxk$ v#9 5 }_$c S#=+"K{F*m7`#%H:NRSp6I?sIՖ{Ap$I$I:QRv2$Z @UJ*$]<FO4IENDB`