Initial YakPanel commit

This commit is contained in:
Niranjan
2026-04-07 02:04:22 +05:30
commit 2826d3e7f3
5359 changed files with 1390724 additions and 0 deletions

View File

@@ -0,0 +1,599 @@
# coding: utf-8
import json
import os
import pwd
import re
from typing import Union, Optional, Tuple, List
import public
public.sys_path_append("class_v2")
from projectModelV2.common import LimitNet, Redirect
from public.exceptions import HintException
try:
from public.hook_import import hook_import
hook_import()
except:
pass
try:
import idna
except:
public.ExecShell('btpip install idna')
import idna
class _ProjectSiteType:
_CONFIG_FILE = "{}/config/project_site.json".format(public.get_panel_path())
allow_type = {"go", "java", "net", "nodejs", "other", "python", "proxy", "html"}
def __init__(self):
self._config = None
@classmethod
def read_conf_file(cls):
default_conf = {
"go": {},
"java": {},
"net": {},
"nodejs": {},
"other": {},
"python": {},
"proxy": {},
"html": {},
}
if not os.path.isfile(cls._CONFIG_FILE):
public.writeFile(cls._CONFIG_FILE, json.dumps(default_conf))
return default_conf
conf_data = public.readFile(cls._CONFIG_FILE)
if not isinstance(conf_data, str):
public.writeFile(cls._CONFIG_FILE, json.dumps(default_conf))
return default_conf
try:
conf = json.loads(conf_data)
except json.JSONDecodeError:
conf = None
if not isinstance(conf, dict):
public.writeFile(cls._CONFIG_FILE, json.dumps(default_conf))
return default_conf
return conf
@property
def config(self):
if self._config is not None:
return self._config
self._config = self.read_conf_file()
return self._config
def save_config_to_file(self):
if self._config:
public.writeFile(self._CONFIG_FILE, json.dumps(self._config))
def get_next_id(self, p_type: str) -> int:
all_ids = [
i["id"] for i in self.config[p_type].values()
]
return max(all_ids + [0]) + 1
def add(self, p_type: str, name: str, ps: str) -> Tuple[bool, str]:
if p_type not in self.allow_type:
return False, "not support type"
if p_type not in self.config:
self.config[p_type] = {}
for t_info in self.config[p_type].values():
if t_info["name"] == name:
return False, "name exists"
next_id = self.get_next_id(p_type)
self.config[p_type][str(next_id)] = {
"id": next_id,
"name": name,
"ps": ps
}
self.save_config_to_file()
return True, ""
def modify(self, p_type: str, t_id: int, name: str, ps: str) -> bool:
if p_type not in self.config:
return False
if str(t_id) not in self.config[p_type]:
return False
self.config[p_type][str(t_id)] = {
"id": t_id,
"name": name,
"ps": ps
}
self.save_config_to_file()
return True
def remove(self, p_type: str, t_id: int) -> bool:
if p_type not in self.config:
return False
if str(t_id) not in self.config[p_type]:
return False
del self.config[p_type][str(t_id)]
self.save_config_to_file()
return True
def find(self, p_type: str, t_id: int) -> Optional[dict]:
if p_type not in self.config:
return None
if str(t_id) not in self.config[p_type]:
return None
return self.config[p_type][str(t_id)]
def list_by_type(self, p_type: str) -> List[dict]:
if p_type not in self.config:
return []
return [
i for i in self.config[p_type].values()
]
class projectBase(LimitNet, Redirect):
def __init__(self):
self._is_nginx_http3 = None
def check_port(self, port):
'''
@name 检查端口是否被占用
@args port:端口号
@return: 被占用返回True否则返回False
@author: lkq 2021-08-28
'''
a = public.ExecShell("netstat -nltp|awk '{print $4}'")
if a[0]:
if re.search(':' + port + '\n', a[0]):
return True
else:
return False
else:
return False
def is_domain(self, domain):
'''
@name 验证域名合法性
@args domain:域名
@return: 合法返回True否则返回False
@author: lkq 2021-08-28
'''
import re
domain_regex = re.compile(r'(?:[A-Z0-9_](?:[A-Z0-9-_]{0,247}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(?<!-))\Z',
re.IGNORECASE)
return True if domain_regex.match(domain) else False
def generate_random_port(self):
'''
@name 生成随机端口
@args
@return: 端口号
@author: lkq 2021-08-28
'''
import random
port = str(random.randint(5000, 10000))
while True:
if not self.check_port(port): break
port = str(random.randint(5000, 10000))
return port
def IsOpen(self, port):
'''
@name 检查端口是否被占用
@args port:端口号
@return: 被占用返回True否则返回False
@author: lkq 2021-08-28
'''
ip = '0.0.0.0'
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip, int(port)))
s.shutdown(2)
return True
except:
return False
@staticmethod
def get_system_user_list(get=None):
"""
默认只返回uid>= 1000 的用户 和 root
get中包含 sys_user 返回 uid>= 100 的用户 和 root
get中包含 all_user 返回所有的用户
"""
sys_user = False
all_user = False
if get is not None:
if hasattr(get, "sys_user"):
sys_user = True
if hasattr(get, "all_user"):
all_user = True
user_set = set()
try:
for tmp_uer in pwd.getpwall():
if tmp_uer.pw_uid == 0:
user_set.add(tmp_uer.pw_name)
elif tmp_uer.pw_uid >= 1000:
user_set.add(tmp_uer.pw_name)
elif sys_user and tmp_uer.pw_uid >= 100:
user_set.add(tmp_uer.pw_name)
elif all_user:
user_set.add(tmp_uer.pw_name)
except Exception:
pass
return list(user_set)
@staticmethod
def _pass_dir_for_user(path_dir: str, user: str):
"""
给某个用户,对应目录的执行权限
"""
import stat
if not os.path.isdir(path_dir):
return
try:
import pwd
uid_data = pwd.getpwnam(user)
uid = uid_data.pw_uid
gid = uid_data.pw_gid
except:
return
if uid == 0:
return
if path_dir[:-1] == "/":
path_dir = path_dir[:-1]
while path_dir != "/":
path_dir_stat = os.stat(path_dir)
if path_dir_stat.st_uid != uid or path_dir_stat.st_gid != gid:
old_mod = stat.S_IMODE(path_dir_stat.st_mode)
if not old_mod & 1:
os.chmod(path_dir, old_mod + 1)
path_dir = os.path.dirname(path_dir)
@staticmethod
def start_by_user(project_id):
file_path = "{}/data/push/tips/project_stop.json".format(public.get_panel_path())
if not os.path.exists(file_path):
data = {}
else:
data_content = public.readFile(file_path)
try:
data = json.loads(data_content)
except json.JSONDecodeError:
data = {}
data[str(project_id)] = False
public.writeFile(file_path, json.dumps(data))
@staticmethod
def stop_by_user(project_id):
file_path = "{}/data/push/tips/project_stop.json".format(public.get_panel_path())
if not os.path.exists(file_path):
data = {}
else:
data_content = public.readFile(file_path)
try:
data = json.loads(data_content)
except json.JSONDecodeError:
data = {}
data[str(project_id)] = True
public.writeFile(file_path, json.dumps(data))
@staticmethod
def is_stop_by_user(project_id):
file_path = "{}/data/push/tips/project_stop.json".format(public.get_panel_path())
if not os.path.exists(file_path):
data = {}
else:
data_content = public.readFile(file_path)
try:
data = json.loads(data_content)
except json.JSONDecodeError:
data = {}
if str(project_id) not in data:
return False
return data[str(project_id)]
def is_nginx_http3(self):
"""判断nginx是否可以使用http3"""
if getattr(self, "_is_nginx_http3", None) is None:
_is_nginx_http3 = public.ExecShell("nginx -V 2>&1| grep 'http_v3_module'")[0] != ''
setattr(self, "_is_nginx_http3", _is_nginx_http3)
return self._is_nginx_http3
@staticmethod
def _check_webserver():
setup_path = public.get_setup_path()
ng_path = setup_path + '/nginx/sbin/nginx'
ap_path = setup_path + '/apache/bin/apachectl'
op_path = '/usr/local/lsws/bin/lswsctrl'
if not os.path.exists(ng_path) and not os.path.exists(ap_path) and not os.path.exists(op_path):
raise HintException(public.lang("Not Found any Web Server"))
tasks = public.M('tasks').where("status!=? AND type!=?", ('1', 'download')).field('id,name').select()
for task in tasks:
name = task["name"].lower()
if name.find("openlitespeed") != -1:
raise HintException(public.lang("Installing OpenLiteSpeed, please wait"))
if name.find("nginx") != -1:
raise HintException(public.lang("Installing Nginx, please wait"))
if name.lower().find("apache") != -1:
raise HintException(public.lang("Installing Apache, please wait"))
# 域名编码转换
@staticmethod
def domain_to_puny_code(domain):
match = re.search(u"[^u\0000-u\001f]+", domain)
if not match:
return domain
try:
if domain.startswith("*."):
return "*." + idna.encode(domain[2:]).decode("utf8")
else:
return idna.encode(domain).decode("utf8")
except:
return domain
# 判断域名是否有效,并返回
def check_domain(self, domain: str) -> Union[str, bool]:
domain = self.domain_to_puny_code(domain)
# 判断通配符域名格式
if domain.find('*') != -1 and domain.find('*.') == -1:
return False
from ssl_domainModelV2.service import DomainValid
if not DomainValid.is_valid_domain(domain):
return False
return domain
def _release_firewall(self, get) -> tuple[bool, str]:
"""尝试放行端口
@author baozi <202-04-18>
@param:
get ( dict_obj ): 创建项目的请求
@return
"""
if getattr(get, "release_firewall", None) in ("0", '', None, False, 0):
return False, public.lang("PS: port not released in firewall, local access only")
port = getattr(get, "port", None)
if port is None:
return True, ""
project_name = getattr(get, "name", "") or getattr(get, "pjname", "") or getattr(get, "project_name", "")
brief = f"Site Project: {public.xsssec(project_name)} release port "
fw_body = {
"protocol": "tcp",
"port": str(port),
"choose": "all",
"domain": "",
"types": "accept",
"strategy": "accept",
"chain": "INPUT",
"brief": brief,
"operation": "add",
}
try:
from firewallModelV2.comModel import main as firewall
try:
ports_exist = firewall().port_rules_list(public.to_dict_obj({
"chain": "ALL",
"query": brief,
}))
# 尝试移除被该项目占用的旧端口
for old_port in public.find_value_by_key(ports_exist, "data", []):
old_port_str = str(old_port.get("Port", ""))
if not old_port_str:
continue
if old_port_str == "80":
continue
if self.IsOpen(old_port):
continue
if old_port.get("Port"):
fw_body["port"] = str(old_port.get("Port", ""))
fw_body["operation"] = "remove"
firewall().set_port_rule(public.to_dict_obj(fw_body))
except:
pass
# add
fw_body["port"] = str(port)
set_res = firewall().set_port_rule(public.to_dict_obj(fw_body))
if set_res.get("status") == 0:
return True, ""
except Exception as e:
import traceback
public.print_log(traceback.format_exc())
public.print_log("_release_firewall error: {}".format(e))
return False, public.lang("PS: port not released in firewall, local access only")
# todo 废弃
def set_daemon_time(self):
"""设置守护进程重启检测时间"""
pass
# todo 废弃
def get_daemon_time(self):
"""获取守护进程重启检测时间"""
pass
# todo 废弃
def _project_mod_type(self) -> Optional[str]:
mod_name = self.__class__.__module__
# "projectModel/javaModel.py" 的格式
if "/" in mod_name:
mod_name = mod_name.rsplit("/", 1)[1]
if mod_name.endswith(".py"):
mod_name = mod_name[:-3]
# "projectModel.javaModel" 的格式
if "." in mod_name:
mod_name = mod_name.rsplit(".", 1)[1]
if mod_name.endswith("Model"):
return mod_name[:-5]
return mod_name
# todo移除到site通用
def project_site_types(self, get=None):
p_type = self._project_mod_type()
res = _ProjectSiteType().list_by_type(p_type)
res_data = [
{"id": 0, "name": "Default category", "ps": ""},
] + res
return public.success_v2(res_data)
# todo移除到site通用
def add_project_site_type(self, get):
try:
type_name = get.type_name.strip()
ps = get.ps.strip()
except AttributeError:
return public.fail_v2("params error")
if not type_name:
return public.fail_v2("name can not be empty")
if len(type_name) > 16:
return public.fail_v2("please do not enter more than 16 characters for the name")
p_type = self._project_mod_type()
flag, msg = _ProjectSiteType().add(p_type, type_name, ps)
if not flag:
return public.fail_v2(msg)
return public.success_v2("Add success")
# todo移除到site通用
def modify_project_site_type(self, get):
try:
type_name = get.type_name.strip()
ps = get.ps.strip()
type_id = int(get.type_id.strip())
except (AttributeError, ValueError, TypeError):
return public.fail_v2("params error")
if not type_name or not type_id:
return public.fail_v2("type_name, type_id can not be empty")
if len(type_name) > 16:
return public.fail_v2("please do not enter more than 16 characters for the name")
p_type = self._project_mod_type()
flag = _ProjectSiteType().modify(p_type, type_id, type_name, ps)
if not flag:
return public.fail_v2("modify error")
return public.success_v2("Modify success")
# todo移除到site通用
def remove_project_site_type(self, get):
try:
type_id = int(get.type_id.strip())
except (AttributeError, ValueError, TypeError):
return public.fail_v2("params error")
p_type = self._project_mod_type()
project_type_map = {
"go": "Go",
"java": "Java",
"net": "net",
"nodejs": "Node",
"other": "Other",
"python": "Python",
"proxy": "proxy",
"html": "html",
}
if p_type not in project_type_map:
return public.fail_v2("params error")
flag = _ProjectSiteType().remove(p_type, type_id)
if not flag:
return public.fail_v2("Delete error")
p_t = project_type_map[p_type]
query_str = 'project_type=? AND type_id=?'
projects = public.M('sites').where(query_str, (p_t, type_id)).field("id").select()
if not projects:
return public.success_v2("Delete success")
project_ids = [i["id"] for i in projects]
update_str = 'project_type=? AND id in ({})'.format(",".join(["?"] * len(project_ids)))
public.M('sites').where(update_str, (p_t, *project_ids)).update({"type_id": 0})
return public.success_v2("Delete success")
# todo移除到site通用
def find_project_site_type(self, type_id: int):
if isinstance(type_id, str):
try:
type_id = int(type_id)
except (AttributeError, ValueError, TypeError):
return None
if type_id == 0:
return {
"id": 0,
"name": "Default category",
"ps": ""
}
p_type = self._project_mod_type()
return _ProjectSiteType().find(p_type, type_id)
# todo移除, 使用batch
def set_project_site_type(self, get):
try:
type_id = int(get.type_id.strip())
if isinstance(get.site_ids, str):
site_ids = json.loads(get.site_ids.strip())
else:
site_ids = get.site_ids
except (AttributeError, ValueError, TypeError):
return public.fail_v2("params error")
if not isinstance(site_ids, list):
return public.fail_v2("params error")
p_type = self._project_mod_type()
project_type_map = {
"go": "Go",
"java": "Java",
"net": "net",
"nodejs": "Node",
"other": "Other",
"python": "Python",
"proxy": "proxy",
"html": "html",
}
if p_type not in project_type_map:
return public.fail_v2("params error")
if not self.find_project_site_type(type_id):
return public.fail_v2("project site type not exists")
p_t = project_type_map[p_type]
query_str = 'project_type=? AND id in ({})'.format(",".join(["?"] * len(site_ids)))
projects = public.M('sites').where(query_str, (p_t, *site_ids)).field("id").select()
if not projects:
return public.fail_v2("no project found")
project_ids = [i["id"] for i in projects]
update_str = 'project_type=? AND id in ({})'.format(",".join(["?"] * len(project_ids)))
public.M('sites').where(update_str, (p_t, *project_ids)).update({"type_id": type_id})
return public.success_v2("Set success")
# todo移除废弃
def batch_set_site_type(self, get):
"""
@name 批量设置网站分类
"""
# v2 site api -> batch_set_site_type
pass

View File

@@ -0,0 +1,935 @@
# coding: utf-8
# -------------------------------------------------------------------
# YakPanel
# -------------------------------------------------------------------
# Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: yakpanel
# -------------------------------------------------------------------
# py virtual environment manager
# ------------------------------
import copy
import json
import os
import re
import shutil
import sys
import tarfile
import threading
import time
from platform import machine
import requests
import traceback
import subprocess
import argparse
from typing import Optional, Tuple, List, Union, Dict, TextIO
from xml.etree import cElementTree
os.chdir("/www/server/panel")
if "class/" not in sys.path:
sys.path.insert(0, "class/")
if "/www/server/panel" not in sys.path:
sys.path.insert(0, "/www/server/panel")
from mod.project.python.pyenv_tool import EnvironmentManager
import public
class _VmSTD:
out = sys.stdout
err = sys.stderr
_vm_std = _VmSTD()
def is_aarch64() -> bool:
_arch = machine().lower()
if _arch in ("aarch64", "arm64"):
return True
return False
def parse_version_to_list(version: str) -> Tuple[int, int, int]:
tmp = version.split(".")
if len(tmp) == 1:
return int(tmp[0]), 0, 0
elif len(tmp) == 2:
return int(tmp[0]), int(tmp[1]), 0
else:
return int(tmp[0]), int(tmp[1]), int(tmp[2])
def _get_index_of_python(url_list: List[str], timeout=10) -> Optional[Dict]:
winner: Dict = {}
done_event = threading.Event()
lock = threading.Lock() # 并发访问winner
def get_result(test_url):
try:
response = requests.get(test_url, timeout=timeout)
text = response.text
if not text:
return
with lock:
# Only record the first successful response
if not winner:
winner["data"] = text
winner["time"] = time.time()
winner["url"] = test_url
done_event.set()
except Exception:
pass
for url in url_list:
threading.Thread(target=get_result, args=(url,), daemon=True).start()
# 阻塞直到第一个成功响应或所有线程超时
done_event.wait(timeout=timeout)
return winner if winner else None
def get_index_of_python() -> Optional[Dict]:
url_list = [
"https://repo.huaweicloud.com/python/", # China mirror (Huawei Cloud)
"https://npmmirror.com/mirrors/python/", # China mirror (Aliyun)
"https://www.python.org/ftp/python/", # Official (官方国际)
"https://mirrors.dotsrc.org/python/", # Europe mirror (欧洲)
]
print(public.lang("Checking network status......"), file=_vm_std.out, flush=True)
res = _get_index_of_python(url_list, timeout=10)
if res is None:
res = _get_index_of_python(url_list, timeout=60)
if res is None:
print(
public.lang("Unable to connect to network, querying CPython interpreter version......"),
file=_vm_std.out,
flush=True
)
return res
def get_index_of_pypy_python() -> Optional[Dict]:
url_list = [
"https://buildbot.pypy.org/mirror/", # PyPy build mirror
"https://downloads.python.org/pypy/", # Official (international)
]
print(public.lang("Checking network status......"), file=_vm_std.out, flush=True)
res = _get_index_of_python(url_list, timeout=10)
if res is None:
res = _get_index_of_python(url_list, timeout=60)
if res is None:
print(
public.lang("Unable to connect to network, querying PyPy interpreter version......"),
file=_vm_std.out,
flush=True
)
return res
class PythonVersion:
def __init__(self, v: str, is_pypy: bool = False, filename: str = None):
self.version = v
self.is_pypy = is_pypy
self.bt_python_path = "/www/server/pyporject_evn/versions"
self.bt_pypy_path = "/www/server/pyporject_evn/pypy_versions"
self._file_name = filename.strip() if isinstance(filename, str) else None
self._ver_t = None
if not os.path.exists(self.bt_python_path):
os.makedirs(self.bt_python_path)
if not os.path.exists(self.bt_pypy_path):
os.makedirs(self.bt_pypy_path)
@property
def ver_t(self) -> Tuple[int, int, int]:
if self._ver_t is not None:
return self._ver_t
self._ver_t = parse_version_to_list(self.version)
return self._ver_t
@property
def installed(self) -> bool:
if self.is_pypy:
return os.path.exists(self.bt_pypy_path + "/" + self.version)
return os.path.exists(self.bt_python_path + "/" + self.version)
@staticmethod
def check(file) -> bool:
print(public.lang("[2/3] Verifying source file......"), file=_vm_std.out, flush=True)
if not os.path.exists(file):
print(public.lang("File does not exist, cannot verify"), file=_vm_std.out, flush=True)
return False
if os.path.getsize(file) < 1024 * 1024 * 10:
print(public.lang("File content is incomplete"), file=_vm_std.out, flush=True)
os.remove(file)
return False
return True
@property
def file_name(self) -> str:
if self._file_name:
return self._file_name
if self.is_pypy and not self._file_name:
raise Exception(public.lang("No file name"))
return "Python-{}.tar.xz".format(self.version)
@staticmethod
def _download_file(dst, url):
print(url, file=_vm_std.out, flush=True)
response = requests.get(url, stream=True, headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0"
})
total_size = int(response.headers.get('content-length', 0))
print(
public.lang("Source file size to download: %.2fM") % (total_size / (1024 * 1024)),
file=_vm_std.out,
flush=True
)
if total_size == 0:
print(public.lang("File download error!"), file=_vm_std.out, flush=True)
downloaded_size = 0
block_size = 1024 * 1024
with open(dst, 'wb') as f:
for data in response.iter_content(block_size):
f.write(data)
downloaded_size += len(data)
progress = (downloaded_size / total_size) * 100
print(
public.lang("Downloading....") + "\t %.2f%% completed" % progress,
end='\r',
flush=True,
file=_vm_std.out
)
response.close()
def download(self, base_url) -> bool:
if self.is_pypy:
cache_dir = os.path.join(self.bt_pypy_path, "cached")
else:
cache_dir = os.path.join(self.bt_python_path, "cached")
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
dst = os.path.join(cache_dir, self.file_name)
if os.path.exists(dst) and os.path.getsize(dst) > 1024 * 1024 * 10:
print(public.lang("[1/3] Using cached source file......"), file=_vm_std.out, flush=True)
return self.check(dst)
print(public.lang("[1/3] Downloading source file......"), file=_vm_std.out, flush=True)
print(public.lang("Downloading source file......"), file=_vm_std.out, flush=True)
down_url = "{}{}/{}".format(base_url, self.version, self.file_name)
if self.is_pypy:
down_url = "{}{}".format(base_url, self.file_name)
self._download_file(dst, down_url)
print(public.lang("Download completed"), file=_vm_std.out, flush=True)
return self.check(dst)
def _install(self, extended_args='') -> bool:
if self.is_pypy:
return self._install_pypy()
print(public.lang("[3/3] Extracting and installing....."), file=_vm_std.out, flush=True)
install_sh = "{}/script/install_python.sh".format(public.get_panel_path())
check_openssl_args, extended_args = self._parse_extended_args(extended_args)
sh_str = "bash {} {} {} '{}'".format(
install_sh,
self.version,
check_openssl_args,
extended_args
)
p = subprocess.Popen(sh_str, stdout=_vm_std.out, stderr=_vm_std.out, shell=True)
p.wait()
if not os.path.exists(self.bt_python_path + "/" + self.version):
return False
self.install_pip_tool(self.bt_python_path + "/" + self.version)
return True
@staticmethod
def _parse_extended_args(extended_args) -> Tuple[str, str]:
rep_openssl = re.compile(r"--with-openssl=(?P<path>\S+)")
res = rep_openssl.search(extended_args)
if res:
path = res.group("path")
if os.path.exists(path):
return "not_check_openssl", extended_args
else:
extended_args = extended_args.replace(res.group(), "")
return "check_openssl", extended_args
return "check_openssl", extended_args
def _install_pypy(self) -> bool:
print(public.lang("[3/3] Extracting and installing....."), file=_vm_std.out, flush=True)
cache_dir = os.path.join(self.bt_pypy_path, "cached")
d_file = os.path.join(cache_dir, self.file_name)
tar = tarfile.open(d_file, "r|bz2")
tar.extractall(self.bt_pypy_path)
tar.close()
os.renames(self.bt_pypy_path + "/" + self.file_name[:-8], self.bt_pypy_path + "/" + self.version)
if not os.path.exists(self.bt_pypy_path + "/" + self.version):
return False
public.writeFile("{}/{}/is_pypy.pl".format(self.bt_pypy_path, self.version), "")
self.install_pip_tool(self.bt_pypy_path + "/" + self.version)
return True
def install_pip_tool(self, python_path):
print(public.lang("Installing pip tool....."), file=_vm_std.out, flush=True)
python_bin = "{}/bin/python3".format(python_path)
pip_bin = "{}/bin/pip3".format(python_path)
if not os.path.exists(python_bin):
python_bin = "{}/bin/python".format(python_path)
pip_bin = "{}/bin/pip".format(python_path)
if self._ver_t[:2] < (3, 4):
_ver = "{}.{}".format(*self._ver_t[:2])
if self._ver_t[:2] in ((3, 1), (3, 0)):
_ver = "3.2"
cache_dir = os.path.join(self.bt_python_path, "cached")
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
get_pip_file = os.path.join(cache_dir, "get-pip{}.py".format(_ver))
if not os.path.exists(get_pip_file):
url = "{}/install/plugin/pythonmamager/pip/get-pip{}.py".format(public.get_url(), _ver)
self._download_file(get_pip_file, url)
shutil.copyfile(get_pip_file, os.path.join(python_path, "get-pip.py"))
sh_str = "{} {}".format(python_bin, os.path.join(python_path, "get-pip.py"))
p = subprocess.Popen(sh_str, stdout=_vm_std.out, stderr=_vm_std.out, shell=True)
p.wait()
print(public.lang("pip tool installation finished"), file=_vm_std.out, flush=True)
else:
sh_str = "{} -m ensurepip".format(python_bin)
p = subprocess.Popen(sh_str, stdout=_vm_std.out, stderr=_vm_std.out, shell=True)
p.wait()
print(public.lang("pip tool installation finished"), file=_vm_std.out, flush=True)
if not os.path.exists(pip_bin):
print(public.lang("pip tool installation failed!!!!"), file=_vm_std.out, flush=True)
else:
self.update_pip_tool(pip_bin)
@staticmethod
def update_pip_tool(pip_bin: str):
update_str = "{} install --upgrade pip setuptools".format(pip_bin)
p = subprocess.Popen(update_str, stdout=_vm_std.out, stderr=_vm_std.out, shell=True)
p.wait()
def install(self, base_url, extended_args='') -> Tuple[bool, str]:
print(public.lang("Start installing......"), file=_vm_std.out, flush=True)
if not self.is_pypy:
dst = os.path.join(self.bt_python_path, self.version)
else:
dst = os.path.join(self.bt_pypy_path, self.version)
if os.path.isdir(dst):
return True, public.lang("Already installed")
# download file
if not self.download(base_url):
return False, public.lang("File download and verification failed!")
# install python
if not self._install(extended_args):
return False, public.lang("Extraction and installation failed!")
print(public.lang("Installation completed!"), file=_vm_std.out, flush=True)
home_path = self.bt_python_path + "/" + self.version
if self.is_pypy:
home_path = self.bt_pypy_path + "/" + self.version
bin_path = "{}/bin/python".format(home_path)
if not os.path.exists(bin_path):
bin_path = "{}/bin/python3".format(home_path)
if not os.path.exists(bin_path):
print(public.lang("Python installation failed!"))
return False, public.lang("Python installation failed!")
if bin_path == "{}/bin/python3".format(home_path):
os.symlink(os.path.realpath(bin_path), "{}/bin/python".format(home_path))
elif bin_path == "{}/bin/python".format(home_path) and not os.path.exists("{}/bin/python3".format(home_path)):
os.symlink(os.path.realpath(bin_path), "{}/bin/python3".format(home_path))
EnvironmentManager.add_python_env("system", bin_path)
return True, ""
@staticmethod
def parse_version(version: str) -> Tuple[bool, str]:
v_rep = re.compile(r"(?P<target>\d+\.\d{1,2}(\.\d{1,2})?)")
v_res = v_rep.search(version)
if v_res:
v = v_res.group("target")
return True, v
else:
return False, ""
class _PyCommandManager(object):
_FORMAT_DATA = """
# Start-Python-Env command line environment settings
export PATH="{}${{PATH}}"
# End-Python-Env
"""
@staticmethod
def check_use():
out, _ = public.ExecShell("lsattr /etc/profile")
return out.find("--i") == -1
def set_python_env(self, python_path: str) -> Tuple[bool, str]:
if python_path is None:
python_path = "" # 清除设置
else:
python_path = python_path + ":"
if not self.check_use():
return False, public.lang("System hardening appears to be enabled, operation not allowed")
try:
rep = re.compile(r'# +Start-Python-Env[^\n]*\n(export +PATH=".*")\n# +End-Python-Env')
profile_data = public.readFile("/etc/profile")
if not isinstance(profile_data, str):
return False, public.lang("Configuration file load error")
tmp_res = rep.search(profile_data)
if tmp_res is not None:
new_profile_data = rep.sub(self._FORMAT_DATA.format(python_path).strip("\n"), profile_data, 1)
else:
new_profile_data = profile_data + self._FORMAT_DATA.format(python_path)
public.writeFile("/etc/profile", new_profile_data)
return True, public.lang("Configuration set successfully")
except:
return False, public.lang("Setting error")
@staticmethod
def get_python_env() -> Optional[str]:
profile_data = public.readFile("/etc/profile")
if not isinstance(profile_data, str):
return None
rep = re.compile(r'# +Start-Python-Env[^\n]*\n(export +PATH="(?P<target>.*)")\n# +End-Python-Env')
tmp_res = rep.search(profile_data)
if tmp_res is None:
return None
path_data = tmp_res.group("target")
python_path = path_data.split(":")[0].strip()
if os.path.exists(python_path):
return python_path
return None
class PYVM(object):
bt_python_path = "/www/server/pyporject_evn/versions"
bt_pypy_path = "/www/server/pyporject_evn/pypy_versions"
_c_py_version_default = (
"2.7.18", "3.0.1", "3.1.5", "3.2.6", "3.3.7", "3.4.10", "3.5.10", "3.6.15", "3.7.17", "3.8.19",
"3.9.19", "3.10.14", "3.11.9", "3.12.3"
)
_pypy_version_default = (
("3.10.14", "pypy3.10-v7.3.16-linux64.tar.bz2"),
("3.9.19", "pypy3.10-v7.3.16-linux64.tar.bz2"),
("3.8.16", "pypy3.8-v7.3.11-linux64.tar.bz2"),
("3.7.13", "pypy3.7-v7.3.9-linux64.tar.bz2"),
("3.6.12", "pypy3.6-v7.3.3-linux64.tar.bz2"),
("2.7.18", "pypy2.7-v7.3.16-linux64.tar.bz2"),
)
def __init__(self, use_shell=False):
if not os.path.exists(self.bt_python_path):
os.makedirs(self.bt_python_path)
if not os.path.exists(self.bt_pypy_path):
os.makedirs(self.bt_pypy_path)
self.use_shell = use_shell
self._cpy_base_url = None
self._pypy_base_url = None
self.stable_versions: Optional[List[PythonVersion]] = None
self._py_cmd_mgr = _PyCommandManager()
self.is_pypy = False
self.async_version = False
def now_python_path(self) -> Optional[str]:
return self._py_cmd_mgr.get_python_env()
def set_python_path(self, python_path) -> Tuple[bool, str]:
return self._py_cmd_mgr.set_python_env(python_path)
@staticmethod
def check_use():
res = os.popen("lsattr /etc/profile")
return res.read().find("--i--") == -1
@property
def base_url(self):
if self.is_pypy:
if self._pypy_base_url is not None:
return self._pypy_base_url
res = get_index_of_pypy_python()
if res is not None:
self._pypy_base_url = res["url"]
return self._pypy_base_url
else:
if self._cpy_base_url is not None:
return self._cpy_base_url
res = get_index_of_python()
if res is not None:
self._cpy_base_url = res["url"]
return self._cpy_base_url
return None
# 获取版本
def get_py_version(self, force=False):
if isinstance(self.stable_versions, list) and len(self.stable_versions) > 1:
return self.stable_versions
if not force:
self.stable_versions = self._get_versions_by_local()
if not force and self.async_version and not self.stable_versions:
self._async_get_versions()
if self.is_pypy:
self.stable_versions = [
PythonVersion(v, is_pypy=True, filename=f) for v, f in self._pypy_version_default
]
return self.stable_versions
self.stable_versions = [PythonVersion(i, is_pypy=False) for i in self._c_py_version_default]
return self.stable_versions
if not self.stable_versions:
if self.use_shell:
print(
public.lang("No local record file found, requesting Python version data from cloud,"
" this may take a while, please wait"),
file=_vm_std.out
)
self.stable_versions, err = self._get_versions_by_cloud()
# 缓存数据到本地
if isinstance(self.stable_versions, list) and len(self.stable_versions) > 1:
self._save_cached(self.stable_versions)
else:
print(err, file=_vm_std.out)
if force and not self.stable_versions:
self.stable_versions = self._get_versions_by_local()
if not self.stable_versions:
self.stable_versions = []
return self.stable_versions
def _async_get_versions(self):
pyvm_mgr = copy.deepcopy(self)
def get_versions():
pyvm_mgr.async_version = False
pyvm_mgr.get_py_version(force=True)
task = threading.Thread(target=get_versions)
task.start()
def _get_versions_by_local(self) -> [Optional[List[PythonVersion]]]:
"""
获取本地稳定版本的缓存数据
"""
local_path = "/www/server/panel/data/pyvm"
if not os.path.exists(local_path):
os.makedirs(local_path)
return None
stable_file = os.path.join(local_path, "stable_versions.txt")
if self.is_pypy:
stable_file = os.path.join(local_path, "pypy_versions.txt")
if not os.path.isfile(stable_file):
return None
with open(stable_file, "r") as f:
if self.is_pypy:
stable_versions = []
for line in f.readlines():
v, filename = line.split("|")
stable_versions.append(PythonVersion(v, is_pypy=True, filename=filename))
else:
stable_versions = [PythonVersion(line.strip()) for line in f.readlines()]
return stable_versions
def _get_versions_by_cloud(self) -> Tuple[Optional[List[PythonVersion]], Optional[str]]:
"""
获取云端支持的稳定版本 排除2.7的稳定版本以外的其他版本
"""
if self.is_pypy:
return self._get_pypy_versions_by_cloud()
res = get_index_of_python()
if res is None:
return None, public.lang("Unable to connect to cloud, please check network connection")
self._base_url: str = res["url"]
data_txt: str = res["data"]
try:
stable_go_versions = self.__parser_xml(data_txt)
return stable_go_versions, None
except:
traceback.print_exc(file=_vm_std.err)
return None, public.lang("Parse error")
def _get_pypy_versions_by_cloud(self) -> Tuple[Optional[List[PythonVersion]], Optional[str]]:
"""
获取云端支持的稳定版本 排除2.7的稳定版本以外的其他版本
"""
if self.base_url is None:
return None, public.lang("Unable to connect to cloud, please check network connection")
try:
stable_versions = []
ver_json = json.loads(requests.get(self.base_url + "versions.json").text)
arch = 'aarch64' if is_aarch64() else "x64"
for i in ver_json:
if i["stable"] is True and i["latest_pypy"] is True:
for file in i["files"]:
if file["arch"] == arch and file["platform"] == "linux":
stable_versions.append(
PythonVersion(i["python_version"], is_pypy=True, filename=file["filename"])
)
return stable_versions, None
except:
traceback.print_exc(file=_vm_std.err)
return None, public.lang("Parse error")
def __parser_xml(self, data_txt: str) -> List[PythonVersion]:
res_list = []
# 只取pre部分
start = data_txt.rfind("<pre>")
end = data_txt.rfind("</pre>") + len("</pre>")
if not start > 0 or not end > 0:
return res_list
data_txt = data_txt[start:end] # 去除hr标签导致的错误
last_2 = {
"data": (2, 0, 0),
"version": None,
}
root = cElementTree.fromstring(data_txt)
for data in root.findall("./a"):
v_str = data.text
if v_str.startswith("2."):
ver = v_str.strip("/")
t_version = parse_version_to_list(ver)
if t_version > last_2["data"]:
last_2["data"] = t_version
last_2["version"] = ver
continue
if v_str.startswith("3."):
p_v = PythonVersion(v_str.strip("/"))
res_list.append(p_v)
continue
if last_2["version"]:
res_list.insert(0, PythonVersion(last_2["version"]))
res_list.sort(key=lambda x: x.ver_t)
need_remove = []
for ver in res_list[::-1]:
if not self.test_last_version_is_stable(ver):
need_remove.append(ver)
else:
break
for ver in need_remove:
res_list.remove(ver)
return res_list
# 检查最新的版本是否有正式发布版本包
def test_last_version_is_stable(self, ver: PythonVersion) -> bool:
response = requests.get("{}{}/".format(self.base_url, ver.version), timeout=10)
data = response.text
if data.find(ver.file_name) != -1:
return True
else:
return False
def _save_cached(self, stable_go_versions: List[PythonVersion]) -> None:
local_path = "/www/server/panel/data/pyvm"
if not os.path.exists(local_path):
os.makedirs(local_path)
if self.is_pypy:
with open(os.path.join(local_path, "pypy_versions.txt"), "w") as f:
for py_v in stable_go_versions:
f.write(py_v.version + "|" + py_v.file_name + "\n")
return
with open(os.path.join(local_path, "stable_versions.txt"), "w") as f:
for py_v in stable_go_versions:
f.write(py_v.version + "\n")
@staticmethod
def del_cached():
local_path = "/www/server/panel/data/pyvm"
stable_file = os.path.join(local_path, "stable_versions.txt")
pypy_file = os.path.join(local_path, "pypy_versions.txt")
if os.path.isfile(stable_file):
os.remove(stable_file)
if os.path.isfile(pypy_file):
os.remove(pypy_file)
def api_ls(self) -> Tuple[List[str], List[str]]:
return [i.strip() for i in os.listdir(self.bt_python_path) if i.startswith("2") or i.startswith("3")], \
[i.strip() for i in os.listdir(self.bt_pypy_path) if i.startswith("2") or i.startswith("3")]
def cmd_ls(self) -> None:
cpy_versions, pypy_versions = self.api_ls()
versions = pypy_versions if self.is_pypy else cpy_versions
if not versions:
print(public.lang("No Python interpreter version is installed"))
return
print("version: ")
for i in versions:
print(" " + i)
def api_ls_remote(self, is_all: bool, force=False) -> Tuple[Optional[List[PythonVersion]], Optional[str]]:
self.get_py_version(force)
self.stable_versions.sort(key=lambda k: k.ver_t, reverse=True)
if is_all:
return self.stable_versions, None
res_new = []
tow_list = [0, 0]
for i in self.stable_versions:
if i.ver_t[:2] != tow_list:
res_new.append(i)
tow_list = i.ver_t[:2]
return res_new, None
def cmd_ls_remote(self, is_all: bool) -> None:
stable, err = self.api_ls_remote(is_all)
cpy_installed, pypy_install = self.api_ls()
installed = pypy_install if self.is_pypy else cpy_installed
if err:
print(public.lang("An error occurred while fetching version information"), file=sys.stderr)
print(err, file=sys.stderr)
print("Stable Version:")
for i in stable:
if i.version in installed:
i.version += " <- installed"
print(" " + i.version)
def _get_version(self, version) -> Union[PythonVersion, str]:
stable, err = self.api_ls_remote(True)
if err:
if self.use_shell:
print(public.lang("An error occurred while fetching version information"), file=_vm_std.err)
print(err, file=_vm_std.err)
return err
for i in stable:
if i.version == version:
return i
if self.use_shell:
print(public.lang("Corresponding version not found"), file=_vm_std.err)
return public.lang("Corresponding version not found")
def re_install_pip_tools(self, version, python_path):
py_v = self._get_version(version)
if isinstance(py_v, str):
return False, py_v
if not py_v.installed:
return False, public.lang("Version not installed")
if not self.is_pypy:
public.ExecShell("rm -rf {}/bin/pip*".format(python_path))
public.ExecShell(
"rm -rf {}/lib/python{}.{}/site-packages/pip*".format(python_path, py_v.ver_t[0], py_v.ver_t[1]))
else:
public.ExecShell("rm -rf {}/bin/pip*".format(python_path))
public.ExecShell(
"rm -rf {}/lib/pypy{}.{}/site-packages/pip*".format(python_path, py_v.ver_t[0], py_v.ver_t[1])
)
py_v.install_pip_tool(python_path)
def api_install(self, version) -> Tuple[bool, str]:
py_v = self._get_version(version)
if isinstance(py_v, str):
return False, py_v
if self.base_url is None:
return False, public.lang("Internet connect error, please check")
return py_v.install(self.base_url)
def cmd_install(self, version, extended_args='') -> None:
py_v = self._get_version(version)
if isinstance(py_v, str):
pass
if self.base_url is None:
print("Internet connect error, please check", file=sys.stderr)
return
_, err = py_v.install(self.base_url, extended_args)
if err:
print(err, file=sys.stderr)
def api_uninstall(self, version: str) -> Tuple[bool, str]:
if not self.is_pypy:
py_path = self.bt_python_path + "/" + version
else:
py_path = self.bt_pypy_path + "/" + version
if os.path.exists(py_path):
import shutil
shutil.rmtree(py_path)
return True, public.lang("Uninstall completed")
def cmd_uninstall(self, version: str) -> None:
_, msg = self.api_uninstall(version)
print(msg, file=sys.stdout)
return
@staticmethod
def set_std(out: TextIO, err: TextIO) -> None:
_vm_std.out = out
_vm_std.err = err
@staticmethod
def _serializer_of_list(s: list, installed: List[str]) -> List[Dict]:
return [{
"version": v.version,
"type": "stable",
"installed": True if v.version in installed else False
} for v in s]
def python_versions(self, refresh=False):
res = {
'status': True,
'cpy_installed': [],
'pypy_installed': [],
'sdk': {
"all": [],
"streamline": [],
"pypy": [],
},
'use': self.now_python_path(),
'command_path': None,
}
sdk = res["sdk"]
old_type = self.is_pypy
cpy_installed, pypy_installed = self.api_ls()
cpy_installed.sort(key=lambda x: int(x.split(".")[1]), reverse=True)
res['cpy_installed'] = cpy_installed
pypy_installed.sort(key=lambda x: int(x.split(".")[1]), reverse=True)
res['pypy_installed'] = pypy_installed
cpy_command_path = [
{
"python_path": os.path.join(self.bt_python_path, i, "bin"),
"type": "version",
"version": i,
"is_pypy": False,
} for i in cpy_installed
]
pypy_command_path = [
{
"python_path": os.path.join(self.bt_pypy_path, i, "bin"),
"type": "version",
"version": i,
"is_pypy": True,
} for i in pypy_installed
]
res["command_path"] = cpy_command_path + pypy_command_path
# cpy
self.is_pypy = False
self.get_py_version(refresh)
self.stable_versions.sort(key=lambda k: k.ver_t, reverse=True)
if not self.stable_versions:
sdk["all"] = sdk["streamline"] = [
{"version": v, "type": "stable", "installed": True} for v in cpy_installed
]
else:
sdk["all"] = self._serializer_of_list(self.stable_versions, cpy_installed)
res_new = []
tow_list = [0, 0]
for i in self.stable_versions:
if i.ver_t[:2] != tow_list:
res_new.append(i)
tow_list = i.ver_t[:2]
sdk["streamline"] = self._serializer_of_list(res_new, cpy_installed)
for i in sdk["streamline"]:
if i.get("version") in cpy_installed:
cpy_installed.remove(i.get("version", ""))
if set(cpy_installed):
for i in set(cpy_installed):
sdk["streamline"].insert(0, {
"version": i,
"type": "stable",
"installed": True
})
# pypy
self.is_pypy = True
self.stable_versions = []
self.get_py_version(refresh)
self.stable_versions.sort(key=lambda k: k.ver_t, reverse=True)
if not self.stable_versions:
sdk["pypy"] = [
{"version": v, "type": "stable", "installed": True} for v in pypy_installed
]
else:
sdk["pypy"] = self._serializer_of_list(self.stable_versions, pypy_installed)
self.stable_versions = None
self.is_pypy = old_type
return res
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='pyvm Python解释器版本管理器')
parser.add_argument('-pypy', action='store_true', help='管理PyPy解释器')
# 添加子命令
subparsers = parser.add_subparsers(title='operation', dest='command')
# 添加ls子命令
subparsers.add_parser('ls', help='展示已安装的Python解释器版本')
subparsers.add_parser('clear_cache', help='清除版本缓存')
# 添加ls子命令
parser_ls_r = subparsers.add_parser('ls-remote', help='展示可安装Python解释器版本默认只展示每个版本中较新的版本')
parser_ls_r.add_argument('-a', action='store_true', help='展示可以安装的所有Python解释器版本')
# 添加install子命令
parser_install = subparsers.add_parser('install', help='安装指定版本')
parser_install.add_argument('version', type=str, help='要安装的Python版本例如3.10.0')
parser_install.add_argument(
'--extend', type=str, default='',
help="传递给Python编译的额外选项用单引号包围多个选项'--disable-ipv6 --enable-loadable-sqlite-extensions'"
)
# 添加uninstall子命令
parser_uninstall = subparsers.add_parser('uninstall', help='卸载并删除指定版本')
parser_uninstall.add_argument('uninstall_param', type=str, help='完整的版本号')
# 添加install_pip子命令
parser_uninstall = subparsers.add_parser('install_pip', help='卸载并删除指定版本')
parser_uninstall.add_argument('install_pip_param', type=str, help='完整的版本号')
input_args = parser.parse_args()
pyvm = PYVM()
if isinstance(pyvm, str):
print(pyvm, file=sys.stderr)
exit(1)
if input_args.pypy:
pyvm.is_pypy = True
pyvm.use_shell = True
if input_args.command == 'clear_cache':
pyvm.del_cached()
elif input_args.command == 'ls':
pyvm.cmd_ls()
elif input_args.command == "ls-remote":
_is_all = True if input_args.a else False
pyvm.cmd_ls_remote(_is_all)
elif input_args.command == "install":
extended = input_args.extend
_flag, _v = PythonVersion.parse_version(input_args.version)
if _flag:
pyvm.cmd_install(_v, extended)
else:
print(public.lang("Version parameter error, should be in the format 1.xx.xx"), file=sys.stderr)
elif input_args.command == "uninstall":
_flag, _v = PythonVersion.parse_version(input_args.uninstall_param)
if _flag:
pyvm.cmd_uninstall(_v)
else:
print(public.lang("Version parameter error, should be in the format 1.xx.xx"), file=sys.stderr)
elif input_args.command == "install_pip":
_flag, _v = PythonVersion.parse_version(input_args.install_pip_param)
if _flag:
pyvm.re_install_pip_tools(_v, pyvm.bt_python_path + "/" + _v)
else:
print(public.lang("Version parameter error, should be in the format 1.xx.xx"), file=sys.stderr)
else:
print(public.lang("Use pyvm -h to view operation commands"), file=sys.stderr)

View File

@@ -0,0 +1,8 @@
from .limit_net import LimitNet
from .redirect import Redirect
__all__ = [
"LimitNet",
"Redirect"
]

View File

@@ -0,0 +1,47 @@
from typing import Optional
class BaseProjectCommon:
setup_path = "/www/server/panel"
_allow_mod_name = {
"go", "java", "net", "nodejs", "other", "python", "proxy",
}
def get_project_mod_type(self) -> Optional[str]:
_mod_name = self.__class__.__module__
# "projectModel/javaModel.py" 的格式
if "/" in _mod_name:
_mod_name = _mod_name.replace("/", ".")
if _mod_name.endswith(".py"):
mod_name = _mod_name[:-3]
else:
mod_name = _mod_name
# "projectModel.javaModel" 的格式
if "." in mod_name:
mod_name = mod_name.rsplit(".", 1)[1]
if mod_name.endswith("Model"):
return mod_name[:-5]
if mod_name in self._allow_mod_name:
return mod_name
return None
@property
def config_prefix(self) -> Optional[str]:
if getattr(self, "_config_prefix_cache", None) is not None:
return getattr(self, "_config_prefix_cache")
p_name = self.get_project_mod_type()
if p_name == "nodejs":
p_name = "node"
if isinstance(p_name, str):
p_name = p_name + "_"
setattr(self, "_config_prefix_cache", p_name)
return p_name
@config_prefix.setter
def config_prefix(self, prefix: str):
setattr(self, "_config_prefix_cache", prefix)

View File

@@ -0,0 +1,239 @@
import os
import re
from typing import Tuple
import public
from .base import BaseProjectCommon
class LimitNet(BaseProjectCommon):
def get_limit_net(self, get):
if public.get_webserver() != 'nginx':
return public.returnMsg(False, 'SITE_NETLIMIT_ERR')
try:
site_id = int(get.site_id)
except (AttributeError, TypeError, ValueError):
return public.returnMsg(False, "The parameter is incorrect")
if self.config_prefix is None:
return public.returnMsg(False, "Unsupported website types")
# 取配置文件
site_name = public.M('sites').where("id=?", (site_id,)).getField('name')
filename = "{}/vhost/nginx/{}{}.conf".format(self.setup_path, self.config_prefix, site_name)
conf = public.readFile(filename)
if not isinstance(conf, str):
return public.returnMsg(False, "Configuration file read error")
# 站点总并发
data = {
'perserver': 0,
'perip': 0,
'limit_rate': 0,
}
rep_per_server = re.compile(r"(?P<prefix>.*)limit_conn +perserver +(?P<target>\d+) *; *", re.M)
tmp_res = rep_per_server.search(conf)
if tmp_res is not None and tmp_res.group("prefix").find("#") == -1: # 有且不是注释
data['perserver'] = int(tmp_res.group("target"))
# IP并发限制
rep_per_ip = re.compile(r"(?P<prefix>.*)limit_conn +perip +(?P<target>\d+) *; *", re.M)
tmp_res = rep_per_ip.search(conf)
if tmp_res is not None and tmp_res.group("prefix").find("#") == -1: # 有且不是注释
data['perip'] = int(tmp_res.group("target"))
# 请求并发限制
rep_limit_rate = re.compile(r"(?P<prefix>.*)limit_rate +(?P<target>\d+)\w+ *; *", re.M)
tmp_res = rep_limit_rate.search(conf)
if tmp_res is not None and tmp_res.group("prefix").find("#") == -1: # 有且不是注释
data['limit_rate'] = int(tmp_res.group("target"))
self._show_limit_net(data)
return data
@staticmethod
def _show_limit_net(data):
values = [
[300, 25, 512],
[200, 10, 1024],
[50, 3, 2048],
[500, 10, 2048],
[400, 15, 1024],
[60, 10, 512],
[150, 4, 1024],
]
for i, c in enumerate(values):
if data["perserver"] == c[0] and data["perip"] == c[1] and data["limit_rate"] == c[2]:
data["value"] = i + 1
break
else:
data["value"] = 0
@staticmethod
def _set_nginx_conf_limit() -> Tuple[bool, str]:
# 设置共享内存
nginx_conf_file = "/www/server/nginx/conf/nginx.conf"
if not os.path.exists(nginx_conf_file):
return False, "nginx配置文件丢失"
nginx_conf = public.readFile(nginx_conf_file)
rep_perip = re.compile(r"\s+limit_conn_zone +\$binary_remote_addr +zone=perip:10m;", re.M)
rep_per_server = re.compile(r"\s+limit_conn_zone +\$server_name +zone=perserver:10m;", re.M)
perip_res = rep_perip.search(nginx_conf)
per_serve_res = rep_per_server.search(nginx_conf)
if perip_res and per_serve_res:
return True, ""
elif perip_res or per_serve_res:
tmp_res = perip_res or per_serve_res
new_conf = nginx_conf[:tmp_res.start()] + (
"\n\t\tlimit_conn_zone $binary_remote_addr zone=perip:10m;"
"\n\t\tlimit_conn_zone $server_name zone=perserver:10m;"
) + nginx_conf[tmp_res.end():]
else:
# 通过检查第一个server的位置
rep_first_server = re.compile(r"http\s*\{(.*\n)*\s*server\s*\{")
tmp_res = rep_first_server.search(nginx_conf)
if tmp_res:
old_http_conf = tmp_res.group()
# 在第一个server项前添加
server_idx = old_http_conf.rfind("server")
new_http_conf = old_http_conf[:server_idx] + (
"\n\t\tlimit_conn_zone $binary_remote_addr zone=perip:10m;"
"\n\t\tlimit_conn_zone $server_name zone=perserver:10m;\n"
) + old_http_conf[server_idx:]
new_conf = rep_first_server.sub(new_http_conf, nginx_conf, 1)
else:
# 在没有配置其他server项目时通过检查include server项目检查
# 通检查 include /www/server/panel/vhost/nginx/*.conf; 位置
rep_include = re.compile(r"http\s*\{(.*\n)*\s*include +/www/server/panel/vhost/nginx/\*\.conf;")
tmp_res = rep_include.search(nginx_conf)
if not tmp_res:
return False, "The global configuration cache configuration failed"
old_http_conf = tmp_res.group()
include_idx = old_http_conf.rfind("include ")
new_http_conf = old_http_conf[:include_idx] + (
"\n\t\tlimit_conn_zone $binary_remote_addr zone=perip:10m;"
"\n\t\tlimit_conn_zone $server_name zone=perserver:10m;\n"
) + old_http_conf[include_idx:]
new_conf = rep_first_server.sub(new_http_conf, nginx_conf, 1)
public.writeFile(nginx_conf_file, new_conf)
if public.checkWebConfig() is not True: # 检测失败,无法添加
public.writeFile(nginx_conf_file, nginx_conf)
return False, "The global configuration cache configuration failed"
return True, ""
# 设置流量限制
def set_limit_net(self, get):
if public.get_webserver() != 'nginx':
return public.returnMsg(False, 'SITE_NETLIMIT_ERR')
try:
site_id = int(get.site_id)
per_server = int(get.perserver)
perip = int(get.perip)
limit_rate = int(get.limit_rate)
except (AttributeError, TypeError, ValueError):
return public.returnMsg(False, "The parameter is incorrect")
if per_server < 1 or perip < 1 or limit_rate < 1:
return public.returnMsg(False, 'The concurrency limit, IP limit, and traffic limit must be greater than 0')
# 取配置文件
site_info = public.M('sites').where("id=?", (site_id,)).find()
if not isinstance(site_info, dict):
return public.returnMsg(False, "站点信息查询错误")
else:
site_name = site_info["name"]
filename = "{}/vhost/nginx/{}{}.conf".format(self.setup_path, self.config_prefix, site_name)
site_conf: str = public.readFile(filename)
if not isinstance(site_conf, str):
return public.returnMsg(False, "配置文件读取错误")
flag, msg = self._set_nginx_conf_limit()
if not flag:
return public.returnMsg(False, msg)
per_server_str = ' limit_conn perserver {};'.format(per_server)
perip_str = ' limit_conn perip {};'.format(perip)
limit_rate_str = ' limit_rate {}k;'.format(limit_rate)
# 请求并发限制
new_conf = site_conf
ssl_end_res = re.search(r"#error_page 404/404.html;[^\n]*\n", new_conf)
if ssl_end_res is None:
return public.returnMsg(False, "未定位到SSL的相关配置添加失败")
ssl_end_idx = ssl_end_res.end()
rep_limit_rate = re.compile(r"(.*)limit_rate +(\d+)\w+ *; *", re.M)
tmp_res = rep_limit_rate.search(new_conf)
if tmp_res is not None :
new_conf = rep_limit_rate.sub(limit_rate_str, new_conf)
else:
new_conf = new_conf[:ssl_end_idx] + limit_rate_str + "\n" + new_conf[ssl_end_idx:]
# IP并发限制
rep_per_ip = re.compile(r"(.*)limit_conn +perip +(\d+) *; *", re.M)
tmp_res = rep_per_ip.search(new_conf)
if tmp_res is not None:
new_conf = rep_per_ip.sub(perip_str, new_conf)
else:
new_conf = new_conf[:ssl_end_idx] + perip_str + "\n" + new_conf[ssl_end_idx:]
rep_per_server = re.compile(r"(.*)limit_conn +perserver +(\d+) *; *", re.M)
tmp_res = rep_per_server.search(site_conf)
if tmp_res is not None:
new_conf = rep_per_server.sub(per_server_str, new_conf)
else:
new_conf = new_conf[:ssl_end_idx] + per_server_str + "\n" + new_conf[ssl_end_idx:]
public.writeFile(filename, new_conf)
is_error = public.checkWebConfig()
if is_error is not True:
public.writeFile(filename, site_conf)
return public.returnMsg(False, 'ERROR:<br><a style="color:red;">' + is_error.replace("\n", '<br>') + '</a>')
public.serviceReload()
public.WriteLog('TYPE_SITE', 'SITE_NETLIMIT_OPEN_SUCCESS', (site_name,))
return public.returnMsg(True, 'Successfully set')
# 关闭流量限制
def close_limit_net(self, get):
if public.get_webserver() != 'nginx':
return public.returnMsg(False, 'SITE_NETLIMIT_ERR')
if self.config_prefix is None:
return public.returnMsg(False, "不支持的网站类型")
try:
site_id = int(get.site_id)
except (AttributeError, TypeError, ValueError):
return public.returnMsg(False, "参数错误")
# 取回配置文件
site_info = public.M('sites').where("id=?", (site_id,)).find()
if not isinstance(site_info, dict):
return public.returnMsg(False, "站点信息查询错误")
else:
site_name = site_info["name"]
filename = "{}/vhost/nginx/{}{}.conf".format(self.setup_path, self.config_prefix, site_name)
site_conf = public.readFile(filename)
if not isinstance(site_conf, str):
return public.returnMsg(False, "配置文件读取错误")
# 清理总并发
rep_limit_rate = re.compile(r"(.*)limit_rate +(\d+)\w+ *; *\n?", re.M)
rep_per_ip = re.compile(r"(.*)limit_conn +perip +(\d+) *; *\n?", re.M)
rep_per_server = re.compile(r"(.*)limit_conn +perserver +(\d+) *; *\n?", re.M)
new_conf = site_conf
new_conf = rep_limit_rate.sub("", new_conf, 1)
new_conf = rep_per_ip.sub("", new_conf, 1)
new_conf = rep_per_server.sub("", new_conf, 1)
public.writeFile(filename, new_conf)
is_error = public.checkWebConfig()
if is_error is not True:
public.writeFile(filename, site_conf)
return public.returnMsg(False, 'ERROR:<br><a style="color:red;">' + is_error.replace("\n", '<br>') + '</a>')
public.serviceReload()
public.WriteLog('TYPE_SITE', 'SITE_NETLIMIT_CLOSE_SUCCESS', (site_name,))
return public.returnMsg(True, 'SITE_NETLIMIT_CLOSE_SUCCESS')

View File

@@ -0,0 +1,832 @@
import os,sys
import re
import json
import hashlib
import time
from typing import Tuple, Optional, Union, Dict, List
from urllib import parse
from itertools import product
import public
from public.validate import Param
# from .base import BaseProjectCommon
class _RealRedirect:
setup_path = "/www/server/panel"
_redirect_conf_file = "{}/data/redirect.conf".format(setup_path)
_ng_domain_format = """
if ($host ~ '^%s'){
return %s %s%s;
}
"""
_ng_path_format = """
rewrite ^%s(.*) %s%s %s;
"""
_ap_domain_format = """
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %%{HTTP_HOST} ^%s [NC]
RewriteRule ^(.*) %s%s [L,R=%s]
</IfModule>
"""
_ap_path_format = """
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteRule ^%s(.*) %s%s [L,R=%s]
</IfModule>
"""
def __init__(self, config_prefix: str):
self._config: Optional[List[Dict[str, Union[str, int]]]] = None
self.config_prefix = config_prefix
self._webserver = None
@property
def webserver(self) -> str:
if self._webserver is not None:
return self._webserver
self._webserver = public.get_webserver()
return self._webserver
@property
def config(self) -> List[Dict[str, Union[str, int, List]]]:
if self._config is not None:
return self._config
try:
self._config = json.loads(public.readFile(self._redirect_conf_file))
except (json.JSONDecodeError, TypeError, ValueError):
self._config = []
if not isinstance(self._config, list):
self._config = []
return self._config
def save_config(self):
if self._config is not None:
return public.writeFile(self._redirect_conf_file, json.dumps(self._config))
def _check_redirect_domain_exist(self, site_name,
redirect_domain: list,
redirect_name: str = None,
is_modify=False) -> Optional[List[str]]:
res = set()
redirect_domain_set = set(redirect_domain)
for c in self.config:
if c["sitename"] != site_name:
continue
if is_modify:
if c["redirectname"] != redirect_name:
res |= set(c["redirectdomain"]) & redirect_domain_set
else:
res |= set(c["redirectdomain"]) & redirect_domain_set
return list(res) if res else None
def _check_redirect_path_exist(self, site_name,
redirect_path: str,
redirect_name: str = None) -> bool:
for c in self.config:
if c["sitename"] == site_name:
if c["redirectname"] != redirect_name and c["redirectpath"] == redirect_path:
return True
return False
@staticmethod
def _parse_url_domain(url: str):
return parse.urlparse(url).netloc
@staticmethod
def _parse_url_path(url: str):
return parse.urlparse(url).path
# 计算name md5
@staticmethod
def _calc_redirect_name_md5(redirect_name) -> str:
md5 = hashlib.md5()
md5.update(redirect_name.encode('utf-8'))
return md5.hexdigest()
def _check_redirect(self, site_name, redirect_name, is_error=False):
for i in self.config:
if i["sitename"] != site_name:
continue
if is_error and "errorpage" in i and i["errorpage"] in [1, '1']:
return i
if i["redirectname"] == redirect_name:
return i
return None
# 创建修改配置检测
def _check_redirect_args(self, get, is_modify=False) -> Union[str, Dict]:
if public.checkWebConfig() is not True:
return public.lang("Config file error; please check the configuration first.")
try:
site_name = get.sitename.strip()
redirect_path = get.redirectpath.strip()
redirect_type = get.redirecttype.strip()
domain_or_path = get.domainorpath.strip()
hold_path = int(get.holdpath)
to_url = ""
to_path = ""
error_page = 0
redirect_domain = []
redirect_name = ""
status_type = 1
if "redirectname" in get and get.redirectname.strip():
redirect_name = get.redirectname.strip()
if "tourl" in get:
to_url = get.tourl.strip()
if "topath" in get:
to_path = get.topath.strip()
if "redirectdomain" in get:
redirect_domain = json.loads(get.redirectdomain.strip())
if "type" in get:
status_type = int(get.type)
if "errorpage" in get:
error_page = int(get.errorpage)
except (AttributeError, ValueError):
return 'The parameter is incorrect'
if not is_modify:
if not redirect_name:
return public.lang("Parameter error: configuration name cannot be empty")
# 检测名称是否重复
if not (3 < len(redirect_name) < 15):
return public.lang("Name length must be greater than 3 and less than 15 characters")
if self._check_redirect(site_name, redirect_name, error_page == 1):
return public.lang("The specified redirect name already exists")
site_info = public.M('sites').where("name=?", (site_name,)).find()
if not isinstance(site_info, dict):
return public.lang("Failed to query site information")
else:
site_name = site_info["name"]
# 检测目标URL格式
rep = r"http(s)?\:\/\/([a-zA-Z0-9][-a-zA-Z0-9]{0,62}\.)+([a-zA-Z0-9][a-zA-Z0-9]{0,62})+.?"
if to_url and not re.match(rep, to_url):
return public.lang("Invalid target URL format: [%s]") % to_url
# 非404页面de重定向检测项
if error_page != 1:
# 检测是否选择域名
if domain_or_path == "domain":
if not redirect_domain:
return public.lang("Please select a redirect domain")
# 检测域名是否已经存在配置文件
repeat_domain = self._check_redirect_domain_exist(site_name, redirect_domain, redirect_name, is_modify)
if repeat_domain:
return public.lang("Redirect domain already exists: %s") % repeat_domain
# 检查目标URL的域名和被重定向的域名是否一样
tu = self._parse_url_domain(to_url)
for d in redirect_domain:
if d == tu:
return public.lang("Domain \"%s\" matches the target domain; please deselect it") % d
else:
if not redirect_path:
return public.lang("Please enter a redirect path")
if redirect_path[0] != "/":
return public.lang("Invalid path format; expected /xxx")
# 检测路径是否有存在配置文件
if self._check_redirect_path_exist(site_name, redirect_path, redirect_name):
return public.lang("Redirect path already exists: %s") % redirect_path
to_url_path = self._parse_url_path(to_url)
if to_url_path.startswith(redirect_path):
return public.lang("Target URL [%s] starts with the redirect path [%s], which will cause a loop") % (to_url_path, redirect_path)
# 404页面重定向检测项
else:
if not to_url and not to_path:
return public.lang("You must choose either the homepage or a custom page")
if to_path:
to_path = "/"
return {
"tourl": to_url,
"topath": to_path,
"errorpage": error_page,
"redirectdomain": redirect_domain,
"redirectname": redirect_name if redirect_name else str(int(time.time())),
"type": status_type,
"sitename": site_name,
"redirectpath": redirect_path,
"redirecttype": redirect_type,
"domainorpath": domain_or_path,
"holdpath": hold_path,
}
def create_redirect(self, get):
res_conf = self._check_redirect_args(get, is_modify=False)
if isinstance(res_conf, str):
return public.returnMsg(False, res_conf)
res = self._set_include(res_conf)
if res is not None:
return public.returnMsg(False, res)
res = self._write_config(res_conf)
if res is not None:
return public.returnMsg(False, res)
self.config.append(res_conf)
self.save_config()
public.serviceReload()
return public.returnMsg(True, public.lang("Created successfully"))
def _set_include(self, res_conf) -> Optional[str]:
flag, msg = self._set_nginx_redirect_include(res_conf)
if not flag:
return msg
flag, msg = self._set_apache_redirect_include(res_conf)
if not flag:
return msg
def _write_config(self, res_conf) -> Optional[str]:
if res_conf["errorpage"] != 1:
res = self.write_nginx_redirect_file(res_conf)
if res is not None:
return res
res = self.write_apache_redirect_file(res_conf)
if res is not None:
return res
else:
self.unset_nginx_404_conf(res_conf["sitename"])
res = self.write_nginx_404_redirect_file(res_conf)
if res is not None:
return res
res = self.write_apache_404_redirect_file(res_conf)
if res is not None:
return res
def modify_redirect(self, get):
"""
@name 修改、启用、禁用重定向
@author hezhihong
@param get.sitename 站点名称
@param get.redirectname 重定向名称
@param get.tourl 目标URL
@param get.redirectdomain 重定向域名
@param get.redirectpath 重定向路径
@param get.redirecttype 重定向类型
@param get.type 重定向状态 0禁用 1启用
@param get.domainorpath 重定向类型 domain 域名重定向 path 路径重定向
@param get.holdpath 保留路径 0不保留 1保留
@return json
"""
# 基本信息检查
res_conf = self._check_redirect_args(get, is_modify=True)
if isinstance(res_conf, str):
return public.returnMsg(False, res_conf)
old_idx = None
for i, conf in enumerate(self.config):
if conf["redirectname"] == res_conf["redirectname"] and conf["sitename"] == res_conf["sitename"]:
old_idx = i
res = self._set_include(res_conf)
if res is not None:
return public.returnMsg(False, res)
res = self._write_config(res_conf)
if res is not None:
return public.returnMsg(False, res)
if old_idx:
self.config[old_idx].update(res_conf)
else:
self.config.append(res_conf)
self.save_config()
public.serviceReload()
return public.returnMsg(True, public.lang("Modification successful"))
def _set_nginx_redirect_include(self, redirect_conf: dict) -> Tuple[bool, str]:
ng_redirect_dir = "%s/vhost/nginx/redirect/%s" % (self.setup_path, redirect_conf["sitename"])
ng_file = "{}/vhost/nginx/{}{}.conf".format(self.setup_path, self.config_prefix, redirect_conf["sitename"])
if not os.path.exists(ng_redirect_dir):
os.makedirs(ng_redirect_dir, 0o600)
ng_conf = public.readFile(ng_file)
if not isinstance(ng_conf, str):
return False, public.lang("Failed to read nginx config file")
rep_include = re.compile(r"\sinclude +.*/redirect/.*\*\.conf;", re.M)
if rep_include.search(ng_conf):
return True, ""
redirect_include = (
"#SSL-END\n"
" # Include redirect rules, commenting out will disable the configured redirect proxy\n"
" include {}/*.conf;"
).format(ng_redirect_dir)
if "#SSL-END" not in ng_conf:
return False, public.lang("Failed to add config: cannot locate SSL config marker")
new_conf = ng_conf.replace("#SSL-END", redirect_include)
public.writeFile(ng_file, new_conf)
if self.webserver == "nginx" and public.checkWebConfig() is not True:
public.writeFile(ng_file, ng_conf)
return False, public.lang("Failed to add config")
return True, ""
def _un_set_nginx_redirect_include(self, redirect_conf: dict) -> Tuple[bool, str]:
ng_file = "{}/vhost/nginx/{}{}.conf".format(self.setup_path, self.config_prefix, redirect_conf["sitename"])
ng_conf = public.readFile(ng_file)
if not isinstance(ng_conf, str):
return False, public.lang("Failed to read nginx config file")
rep_include = re.compile(r"(#(.*)\n)?\s*include +.*/redirect/.*\*\.conf;")
if not rep_include.search(ng_conf):
return True, ""
new_conf = rep_include.sub("", ng_conf, 1)
public.writeFile(ng_file, new_conf)
if self.webserver == "nginx" and public.checkWebConfig() is not True:
public.writeFile(ng_file, ng_conf)
return False, public.lang("Failed to remove config")
return True, ""
def _set_apache_redirect_include(self, redirect_conf: dict) -> Tuple[bool, str]:
ap_redirect_dir = "%s/vhost/apache/redirect/%s" % (self.setup_path, redirect_conf["sitename"])
ap_file = "{}/vhost/apache/{}{}.conf".format(self.setup_path, self.config_prefix, redirect_conf["sitename"])
if not os.path.exists(ap_redirect_dir):
os.makedirs(ap_redirect_dir, 0o600)
ap_conf = public.readFile(ap_file)
if not isinstance(ap_conf, str):
return False, public.lang("Failed to read apache config file")
rep_include = re.compile(r"\sIncludeOptional +.*/redirect/.*\*\.conf", re.M)
# public.print_log(list(rep_include.finditer(ap_conf)))
include_count = len(list(rep_include.finditer(ap_conf)))
if ap_conf.count("</VirtualHost>") == include_count:
return True, ""
if include_count > 0:
# 先清除已有的配置
self._un_set_apache_redirect_include(redirect_conf)
rep_custom_log = re.compile(r"CustomLog .*\n")
rep_deny_files = re.compile(r"\n\s*#DENY FILES")
include_conf = (
"\n # Include redirect rules, commenting out will disable the configured redirect proxy\n"
" IncludeOptional {}/*.conf\n"
).format(ap_redirect_dir)
new_conf = None
def set_by_rep_idx(rep: re.Pattern, use_start: bool) -> bool:
new_conf_list = []
last_idx = 0
for tmp in rep.finditer(ap_conf):
new_conf_list.append(ap_conf[last_idx:tmp.start()])
if use_start:
new_conf_list.append(include_conf)
new_conf_list.append(tmp.group())
else:
new_conf_list.append(tmp.group())
new_conf_list.append(include_conf)
last_idx = tmp.end()
new_conf_list.append(ap_conf[last_idx:])
nonlocal new_conf
new_conf = "".join(new_conf_list)
public.writeFile(ap_file, new_conf)
if self.webserver == "apache" and public.checkWebConfig() is not True:
public.writeFile(ap_file, ap_conf)
return False
return True
if set_by_rep_idx(rep_custom_log, False) and rep_include.search(new_conf):
return True, ""
if set_by_rep_idx(rep_deny_files, True) and rep_include.search(new_conf):
return True, ""
return False, public.lang("Failed to set config")
def _un_set_apache_redirect_include(self, redirect_conf: dict) -> Tuple[bool, str]:
ap_file = "{}/vhost/apache/{}{}.conf".format(self.setup_path, self.config_prefix, redirect_conf["sitename"])
ap_conf = public.readFile(ap_file)
if not isinstance(ap_conf, str):
return False, public.lang("Failed to read apache config file")
rep_include = re.compile(r"(#(.*)\n)?\s*IncludeOptional +.*/redirect/.*\*\.conf")
if not rep_include.search(ap_conf):
return True, ""
new_conf = rep_include.sub("", ap_conf)
public.writeFile(ap_file, new_conf)
if self.webserver == "apache" and public.checkWebConfig() is not True:
public.writeFile(ap_file, ap_conf)
return False, public.lang("Failed to remove config")
return True, ""
def write_nginx_redirect_file(self, redirect_conf: dict) -> Optional[str]:
conf_file = "{}/vhost/nginx/redirect/{}/{}_{}.conf".format(
self.setup_path, redirect_conf["sitename"], self._calc_redirect_name_md5(redirect_conf["redirectname"]),
redirect_conf["sitename"]
)
if redirect_conf["type"] == 1:
to_url = redirect_conf["tourl"]
conf_list = ["#REWRITE-START"]
if redirect_conf["domainorpath"] == "domain":
hold_path = "$request_uri" if redirect_conf["holdpath"] == 1 else ""
for sd in redirect_conf["redirectdomain"]:
if sd.startswith("*."):
sd = r"[\w.]+\." + sd[2:]
conf_list.append(self._ng_domain_format % (
sd, redirect_conf["redirecttype"], to_url, hold_path
))
else:
redirect_path = redirect_conf["redirectpath"]
if redirect_conf["redirecttype"] == "301":
redirect_type = "permanent"
else:
redirect_type = "redirect"
hold_path = "$1" if redirect_conf["holdpath"] == 1 else ""
conf_list.append(self._ng_path_format % (redirect_path, to_url, hold_path, redirect_type))
conf_list.append("#REWRITE-END")
conf_data = "\n".join(conf_list)
public.writeFile(conf_file, conf_data)
if self.webserver == "nginx":
isError = public.checkWebConfig()
if isError is not True:
if os.path.exists(conf_file):
os.remove(conf_file)
return 'ERROR: 配置出错<br><a style="color:red;">' + isError.replace("\n", '<br>') + '</a>'
else:
if os.path.exists(conf_file):
os.remove(conf_file)
def write_apache_redirect_file(self, redirect_conf: dict) -> Optional[str]:
conf_file = "{}/vhost/apache/redirect/{}/{}_{}.conf".format(
self.setup_path, redirect_conf["sitename"], self._calc_redirect_name_md5(redirect_conf["redirectname"]),
redirect_conf["sitename"]
)
if redirect_conf["type"] != 1:
if os.path.exists(conf_file):
os.remove(conf_file)
return
to_url = redirect_conf["tourl"]
conf_list = ["#REWRITE-START"]
hold_path = "$1" if redirect_conf["holdpath"] == 1 else ""
if redirect_conf["domainorpath"] == "domain":
for sd in redirect_conf["redirectdomain"]:
if sd.startswith("*."):
sd = r"[\w.]+\." + sd[2:]
conf_list.append(self._ap_domain_format % (
sd, to_url, hold_path, redirect_conf["redirecttype"]
))
else:
redirect_path = redirect_conf["redirectpath"]
conf_list.append(self._ap_path_format % (redirect_path, to_url, hold_path, redirect_conf["redirecttype"]))
conf_list.append("#REWRITE-END")
public.writeFile(conf_file, "\n".join(conf_list))
if self.webserver == "apache":
isError = public.checkWebConfig()
if isError is not True:
if os.path.exists(conf_file):
os.remove(conf_file)
return 'ERROR: 配置出错<br><a style="color:red;">' + isError.replace("\n", '<br>') + '</a>'
def unset_nginx_404_conf(self, site_name):
"""
清理已有的 404 页面 配置
"""
need_clear_files = [
"{}/vhost/nginx/{}{}.conf".format(self.setup_path, self.config_prefix, site_name),
"{}/vhost/nginx/rewrite/{}{}.conf".format(self.setup_path, self.config_prefix, site_name),
]
rep_error_page = re.compile(r'(?P<prefix>.*)error_page +404 +/404\.html[^\n]*\n', re.M)
rep_location_404 = re.compile(r'(?P<prefix>.*)location += +/404\.html[^}]*}')
clear_files = [
{
"data": public.readFile(i),
"path": i,
} for i in need_clear_files
]
for file_info, rep in product(clear_files, (rep_error_page, rep_location_404)):
if not isinstance(file_info["data"], str):
continue
tmp_res = rep.search(file_info["data"])
if not tmp_res or tmp_res.group("prefix").find("#") != -1:
continue
file_info["data"] = rep.sub("", file_info["data"])
for i in clear_files:
if not isinstance(i["data"], str):
continue
public.writeFile(i["path"], i["data"])
def write_nginx_404_redirect_file(self, redirect_conf: dict) -> Optional[str]:
"""
设置nginx 404重定向
"""
r_name_md5 = self._calc_redirect_name_md5(redirect_conf["redirectname"])
file_path = "{}/vhost/nginx/redirect/{}".format(self.setup_path, redirect_conf["sitename"])
file_name = '%s_%s.conf' % (r_name_md5, redirect_conf["sitename"])
conf_file = os.path.join(file_path, file_name)
if redirect_conf["type"] != 1:
if os.path.exists(conf_file):
os.remove(conf_file)
return
_path = redirect_conf["tourl"] if redirect_conf["tourl"] else redirect_conf["topath"]
conf_data = (
'#REWRITE-START\n'
'error_page 404 = @notfound;\n'
'location @notfound {{\n'
' return {} {};\n'
'}}\n#REWRITE-END'
).format(redirect_conf["redirecttype"], _path)
public.writeFile(conf_file, conf_data)
if self.webserver == "nginx":
isError = public.checkWebConfig()
if isError is not True:
if os.path.exists(conf_file):
os.remove(conf_file)
return 'ERROR: 配置出错<br><a style="color:red;">' + isError.replace("\n", '<br>') + '</a>'
def write_apache_404_redirect_file(self, redirect_conf: dict) -> Optional[str]:
"""
设置apache 404重定向
"""
r_name_md5 = self._calc_redirect_name_md5(redirect_conf["redirectname"])
conf_file = "{}/vhost/apache/redirect/{}/{}_{}.conf".format(
self.setup_path, redirect_conf["sitename"], r_name_md5, redirect_conf["sitename"]
)
if redirect_conf["type"] != 1:
if os.path.exists(conf_file):
os.remove(conf_file)
return
_path = redirect_conf["tourl"] if redirect_conf["tourl"] else redirect_conf["topath"]
conf_data = """
#REWRITE-START
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteCond %{{REQUEST_FILENAME}} !-f
RewriteCond %{{REQUEST_FILENAME}} !-d
RewriteRule . {} [L,R={}]
</IfModule>
#REWRITE-END
""".format(_path, redirect_conf["redirecttype"])
public.writeFile(conf_file, conf_data)
if self.webserver == "apache":
isError = public.checkWebConfig()
if isError is not True:
if os.path.exists(conf_file):
os.remove(conf_file)
return 'ERROR: 配置出错<br><a style="color:red;">' + isError.replace("\n", '<br>') + '</a>'
def remove_redirect(self, get, multiple=None):
try:
site_name = get.sitename.strip()
redirect_name = get.redirectname.strip()
except AttributeError:
return public.returnMsg(False, public.lang("Parameter error"))
target_idx = None
have_other_redirect = False
target_conf = None
for i, conf in enumerate(self.config): # index i
if conf["redirectname"] != redirect_name and conf["sitename"] == site_name:
have_other_redirect = True
if conf["redirectname"] == redirect_name and conf["sitename"] == site_name:
target_idx = i
target_conf = conf
if target_idx is None: # target_idx 可以为0
return public.returnMsg(False, public.lang("No matching configuration found"))
r_md5_name = self._calc_redirect_name_md5(target_conf["redirectname"])
public.ExecShell("rm -f %s/vhost/nginx/redirect/%s/%s_%s.conf" % (
self.setup_path, site_name, r_md5_name, site_name))
public.ExecShell("rm -f %s/vhost/apache/redirect/%s/%s_%s.conf" % (
self.setup_path, site_name, r_md5_name, site_name))
if not have_other_redirect:
self._un_set_apache_redirect_include(target_conf)
self._un_set_nginx_redirect_include(target_conf)
del self.config[target_idx]
self.save_config()
if not multiple:
public.serviceReload()
return public.returnMsg(True, public.lang("Deleted successfully"))
def mutil_remove_redirect(self, get):
try:
redirect_names = json.loads(get.redirectnames.strip())
site_name = json.loads(get.sitename.strip())
except (AttributeError, json.JSONDecodeError, TypeError):
return public.returnMsg(False, public.lang("Parameter error"))
del_successfully = []
del_failed = {}
get_obj = public.dict_obj()
for redirect_name in redirect_names:
get_obj.redirectname = redirect_name
get_obj.sitename = site_name
try:
result = self.remove_redirect(get, multiple=1)
if not result['status']:
del_failed[redirect_name] = result['msg']
continue
del_successfully.append(redirect_name)
except:
del_failed[redirect_name] = public.lang("An error occurred while deleting; please try again")
public.serviceReload()
msg = 'Successfully deleted redirects [ {} ]'.format(','.join(del_successfully))
if del_failed:
msg += '; Failed to delete redirects: '
for k in del_failed:
msg += ' {} => {}; '.format(k, del_failed[k])
return {
'status': True,
'msg': msg,
}
def get_redirect_list(self, get):
try:
error_page = None
site_name = get.sitename.strip()
if "errorpage" in get:
error_page = int(get.errorpage)
except Exception:
return public.return_message(-1,0, "parameter error")
redirect_list = []
webserver = public.get_webserver()
if webserver == 'openlitespeed':
webserver = 'apache'
for conf in self.config:
if conf["sitename"] != site_name:
continue
if error_page is not None and error_page != int(conf['errorpage']):
continue
if 'errorpage' in conf and conf['errorpage'] in [1, '1']:
conf['redirectdomain'] = ['404 page']
md5_name = self._calc_redirect_name_md5(conf['redirectname'])
conf["redirect_conf_file"] = "%s/vhost/%s/redirect/%s/%s_%s.conf" % (
self.setup_path, webserver, site_name, md5_name, site_name)
conf["type"] = 1 if os.path.isfile(conf["redirect_conf_file"]) else 0
redirect_list.append(conf)
return public.returnMsg(True, redirect_list)
def remove_redirect_by_project_name(self, project_name):
for i in range(len(self.config) - 1, -1, -1):
if self.config[i]["sitename"] == project_name:
del self.config[i]
self.save_config()
m_path = self.setup_path + '/vhost/nginx/redirect/' + project_name
if os.path.exists(m_path):
public.ExecShell("rm -rf %s" % m_path)
m_path = self.setup_path + '/vhost/apache/redirect/' + project_name
if os.path.exists(m_path):
public.ExecShell("rm -rf %s" % m_path)
def test_api_warp(fn):
def inner(*args, **kwargs):
try:
return fn(*args, **kwargs)
except:
public.print_log(public.get_error_info())
return inner
class BaseProjectCommon:
setup_path = "/www/server/panel"
_allow_mod_name = {
"go", "java", "net", "nodejs", "other", "python", "proxy",
}
def get_project_mod_type(self) -> Optional[str]:
_mod_name = self.__class__.__module__
# "projectModel/javaModel.py" 的格式
if "/" in _mod_name:
_mod_name = _mod_name.replace("/", ".")
if _mod_name.endswith(".py"):
mod_name = _mod_name[:-3]
else:
mod_name = _mod_name
# "projectModel.javaModel" 的格式
if "." in mod_name:
mod_name = mod_name.rsplit(".", 1)[1]
if mod_name.endswith("Model"):
return mod_name[:-5]
if mod_name in self._allow_mod_name:
return mod_name
return None
@property
def config_prefix(self) -> Optional[str]:
if getattr(self, "_config_prefix_cache", None) is not None:
return getattr(self, "_config_prefix_cache")
p_name = self.get_project_mod_type()
if p_name == "nodejs":
p_name = "node"
if isinstance(p_name, str):
p_name = p_name + "_"
setattr(self, "_config_prefix_cache", p_name)
return p_name
@config_prefix.setter
def config_prefix(self, prefix: str):
setattr(self, "_config_prefix_cache", prefix)
class Redirect(BaseProjectCommon):
"""项目重定向管理"""
@staticmethod
def aa_return(fun):
"""统一返回格式"""
def inner(*args, **kwargs):
try:
res = fun(*args, **kwargs)
if res and isinstance(res, dict):
msg = res["msg"] # 故意抛异常
msg = public.gettext_msg(msg) if isinstance(msg, str) else msg
return public.return_message(
0 if res.get("status", False) else -1, 0, msg
)
else:
public.print_log("Error: Unexpected return value: {}".format(res))
return public.return_message(0, 0, res)
except Exception as e:
public.print_log("Redirect return format Error: {}".format(e))
return fun(*args, **kwargs)
return inner
@aa_return
def remove_redirect_by_project_name(self, project_name):
if not isinstance(self.config_prefix, str):
return None
return _RealRedirect(self.config_prefix).remove_redirect_by_project_name(project_name)
@aa_return
def create_project_redirect(self, get):
if not isinstance(self.config_prefix, str):
return public.returnMsg(False, "Unsupported website type")
return _RealRedirect(self.config_prefix).create_redirect(get)
@aa_return
def modify_project_redirect(self, get):
# 批量走site老接口
if not isinstance(self.config_prefix, str):
return public.returnMsg(False, "Unsupported website type")
return _RealRedirect(self.config_prefix).modify_redirect(get)
@aa_return
def remove_project_redirect(self, get):
if not isinstance(self.config_prefix, str):
return public.returnMsg(False, "Unsupported website type")
return _RealRedirect(self.config_prefix).remove_redirect(get)
@aa_return
def mutil_remove_project_redirect(self, get):
if not isinstance(self.config_prefix, str):
return public.returnMsg(False, "Unsupported website type")
return _RealRedirect(self.config_prefix).mutil_remove_redirect(get)
@aa_return
def get_project_redirect_list(self, get):
# 校验参数
try:
get.validate([
Param('sitename').String(),
], [
public.validate.trim_filter(),
])
except Exception as ex:
public.print_log("error info: {}".format(ex))
return public.return_message(-1, 0, str(ex))
self.config_prefix='proxy_'
if not isinstance(self.config_prefix, str):
return public.return_message(-1,0, "Unsupported website type")
return _RealRedirect(self.config_prefix).get_redirect_list(get)

View File

@@ -0,0 +1,58 @@
# coding: utf-8
# -------------------------------------------------------------------
# YakPanel
# -------------------------------------------------------------------
# Copyright (c) 2015-2017 YakPanel(www.yakpanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: zouhw <zhw@yakpanel.com>
# -------------------------------------------------------------------
# ------------------------------
# 项目管理控制器
# ------------------------------
import os ,public ,json ,re ,time #line:13
class main :#line:15
def __init__ (O00000000O0OO000O ):#line:17
pass #line:18
def model (O0O0OOO0OO0OO00OO ,OO0O0O000O00O00O0 ):#line:20
""#line:29
import panelPlugin #line:30
OO00OO0OOOO0OO0O0 =public .to_dict_obj ({})#line:31
OO00OO0OOOO0OO0O0 .focre =1 #line:32
O0OO000000O0000O0 =panelPlugin .panelPlugin ().get_soft_list (OO00OO0OOOO0OO0O0 )#line:33
__OO000O0000OO000OO =int (O0OO000000O0000O0 ['ltd'])>1 #line:34
try :#line:40
OO0O0O000O00O00O0 .def_name =OO0O0O000O00O00O0 .dk_def_name #line:41
OO0O0O000O00O00O0 .mod_name =OO0O0O000O00O00O0 .dk_model_name #line:42
if OO0O0O000O00O00O0 ['mod_name']in ['base']:return public .return_status_code (1000 ,'Wrong call!')#line:43
public .exists_args ('def_name,mod_name',OO0O0O000O00O00O0 )#line:44
if OO0O0O000O00O00O0 ['def_name'].find ('__')!=-1 :return public .return_status_code (1000 ,'The called method name cannot contain the "__" characterrong call!')#line:45
if not re .match (r"^\w+$",OO0O0O000O00O00O0 ['mod_name']):return public .return_status_code (1000 ,r'The called module name cannot contain characters other than \w')#line:46
if not re .match (r"^\w+$",OO0O0O000O00O00O0 ['def_name']):return public .return_status_code (1000 ,r'The called module name cannot contain characters other than \w')#line:47
except :#line:48
return public .get_error_object ()#line:49
O0OOO0O0O00O00O0O ="dk_{}".format (OO0O0O000O00O00O0 ['mod_name'].strip ())#line:51
OO00OO0OOOO0OOOOO =OO0O0O000O00O00O0 ['def_name'].strip ()#line:52
OO00OO0O0OOOOO000 ="{}/projectModel/bt_docker/{}.py".format (public .get_class_path (),O0OOO0O0O00O00O0O )#line:55
if not os .path .exists (OO00OO0O0OOOOO000 ):#line:56
return public .return_status_code (1003 ,O0OOO0O0O00O00O0O )#line:57
OO00O00OOOOOO0000 =public .get_script_object (OO00OO0O0OOOOO000 )#line:59
if not OO00O00OOOOOO0000 :return public .return_status_code (1000 ,'{} model not found'.format (O0OOO0O0O00O00O0O ))#line:60
OOO0O000O0OOOOO0O =getattr (OO00O00OOOOOO0000 .main (),OO00OO0OOOO0OOOOO ,None )#line:61
if not OOO0O000O0OOOOO0O :return public .return_status_code (1000 ,'{} method not found in {} model'.format (O0OOO0O0O00O00O0O ,OO00OO0OOOO0OOOOO ))#line:62
O00O0O00O000O0000 ='{}_{}_LAST'.format (O0OOO0O0O00O00O0O .upper (),OO00OO0OOOO0OOOOO .upper ())#line:76
O0OO0OOOOO00OOOO0 =public .exec_hook (O00O0O00O000O0000 ,OO0O0O000O00O00O0 )#line:77
if isinstance (O0OO0OOOOO00OOOO0 ,public .dict_obj ):#line:78
OOO0OOOOOOO000O0O =O0OO0OOOOO00OOOO0 #line:79
elif isinstance (O0OO0OOOOO00OOOO0 ,dict ):#line:80
return O0OO0OOOOO00OOOO0 #line:81
elif isinstance (O0OO0OOOOO00OOOO0 ,bool ):#line:82
if not O0OO0OOOOO00OOOO0 :#line:83
return public .return_data (False ,{},error_msg ='Pre-HOOK interrupt operation')#line:84
OO000OOOO000OOO0O =OOO0O000O0OOOOO0O (OO0O0O000O00O00O0 )#line:87
O00O0O00O000O0000 ='{}_{}_END'.format (O0OOO0O0O00O00O0O .upper (),OO00OO0OOOO0OOOOO .upper ())#line:90
O0O000OO0O00O0OOO =public .to_dict_obj ({'args':OO0O0O000O00O00O0 ,'result':OO000OOOO000OOO0O })#line:94
O0OO0OOOOO00OOOO0 =public .exec_hook (O00O0O00O000O0000 ,O0O000OO0O00O0OOO )#line:95
if isinstance (O0OO0OOOOO00OOOO0 ,dict ):#line:96
OO000OOOO000OOO0O =O0OO0OOOOO00OOOO0 ['result']#line:97
return OO000OOOO000OOO0O #line:98

View File

@@ -0,0 +1,742 @@
# coding: utf-8
# -------------------------------------------------------------------
# YakPanel-基础网站数据统计模块
# 该模块用于统计基础网站数据包括IP数量、流量、访问量、PV、UV等数据。
# -------------------------------------------------------------------
# Copyright (c) 2015-2099 YakPanel(https://www.yakpanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: wpl <wpl@yakpanel.com>
# -------------------------------------------------------------------
import json
import os
import re
import sys
from datetime import datetime, timedelta
from safeModel.base import safeBase
os.chdir("/www/server/panel")
sys.path.append("class_v2/")
import db
import public
class main(safeBase):
# ------------------------ 常量设置 ------------------------
TOTAL_DIR = '/www/server/site_total/data/total'
SUMMARY_DIR = '/www/server/site_total/data/summary'
def __init__(self):
pass
# ------------------------ 对外接口 ------------------------
def get_overview(self, get=None):
"""
@name 总概览接口(全站三日总览 + 网站排名TOP5 + 全站近7天趋势
@return public.return_data(True, data)
安全与约束:
- Top5站点固定按pv升序排序用户确认默认升序指标需传参支持traffic/req_count/ip_count/uv/pv
- 日期以服务器本地时间为准:今日/昨日/前日
- 近7天包含今日在内的连续7个自然日
"""
public.set_module_logs('site_total', 'get_overview', 1)
today, yesterday, day_before = self._get_three_days()
# 三日总览(全站)
today_total = self._get_all_sites_total_for_date(today)
yesterday_total = self._get_all_sites_total_for_date(yesterday)
day_before_total = self._get_all_sites_total_for_date(day_before)
compare = self._compute_compare(today_total, yesterday_total)
overview_three_days = {
'today': self._format_daily_stat(today_total, include_compare=compare),
'yesterday': self._format_daily_stat(yesterday_total),
'day_before': self._format_daily_stat(day_before_total)
}
# Top5排序参数支持传递默认 pv/asc
metric = 'pv'
order = 'asc'
if get:
try:
gm = getattr(get, 'metric', None)
go = getattr(get, 'order', None)
if gm is None and isinstance(get, dict):
gm = get.get('metric')
if go is None and isinstance(get, dict):
go = get.get('order')
if gm:
metric = str(gm)
if go:
order = str(go)
except Exception:
pass
valid_metrics = ['traffic', 'req_count', 'ip_count', 'uv', 'pv']
valid_orders = ['asc', 'desc']
if metric not in valid_metrics:
metric = 'pv'
if order not in valid_orders:
order = 'asc'
# Top5 当天(支持传递 metric/order默认 pv/asc
top5 = self._get_top_sites_for_date(today, metric=metric, order=order, limit=5)
# 近7天趋势全站
trend_points = self._build_trend_7days_all()
rec_status, detail_id = self._get_rec_status_detail()
data = {
'date_range': {
'today': today,
'yesterday': yesterday,
'day_before': day_before
},
'overview_three_days': overview_three_days,
'top5_sites': {
'metric': metric,
'order': order,
'timeframe': 'today',
# 'range': {'date': today},
'items': top5
},
'trend_7days': {
'timeframe': 'last_7_days',
'points': trend_points
},
'rec_status': rec_status,
'detail_id': detail_id
}
return public.return_message(0, 0, data)
def get_site_overview(self, get):
"""
@name 指定站点数据概览接口(单站三日总览 + 单站近7天趋势
@param get.site_name 站点名(必填,校验:仅允许字母、数字、点、短横线、下划线)
@return public.return_data(True, data)
"""
# 监控报表获取数据(确保数据一致)
public.set_module_logs('site_total', 'get_site_overview', 1)
site_name = getattr(get, 'site_name', None)
if not site_name:
return public.return_message(-1, 0, 'The site name is illegal or missing!')
today, yesterday, day_before = self._get_three_days()
# 三日总览(单站)
today_total = self._get_site_total_for_date(site_name, today)
yesterday_total = self._get_site_total_for_date(site_name, yesterday)
day_before_total = self._get_site_total_for_date(site_name, day_before)
compare = self._compute_compare(today_total, yesterday_total)
overview_three_days = {
'today': self._format_daily_stat(today_total, include_compare=compare),
'yesterday': self._format_daily_stat(yesterday_total),
'day_before': self._format_daily_stat(day_before_total)
}
# 近7天趋势单站
trend_points = self._build_trend_7days_site(site_name)
# 检查config配置是否需要更新
self._check_config()
data = {
'site': site_name,
'date_range': {
'today': today,
'yesterday': yesterday,
'day_before': day_before
},
'overview_three_days': overview_three_days,
'trend_7days': {
'site': site_name,
'timeframe': 'last_7_days',
'points': trend_points
}
}
return public.return_message(0, 0, data)
# def receive_products(self, get):
# """
# @name 领取产品接口
# @param get.detail_id 活动详情ID必填
# @return public.return_data(True, data)
# """
# try:
# u = public.get_user_info()
# if not isinstance(u, dict):
# return public.return_message(-1, 0, 'User information acquisition failed!')
# serverid = u.get('serverid')
# access_key = u.get('access_key')
# uid = u.get('uid')
# if not serverid or not access_key or uid is None:
# return public.return_message(-1, 0, 'Missing parameters')
# detail_id = None
# try:
# detail_id = getattr(get, 'detail_id', None)
# except Exception:
# detail_id = None
# if detail_id is None and isinstance(get, dict):
# detail_id = get.get('detail_id')
# if detail_id is None:
# return public.return_message(-1, 0, '缺少detail_id')
# mac = public.get_mac_address()
# payload = {
# 'serverid': serverid,
# 'access_key': access_key,
# 'uid': uid,
# 'detail_id': detail_id,
# 'mac': mac
# }
# url = 'https://www.yakpanel.com/newapi/activity/panelapi/receive_products'
# res = public.httpPost(url, payload)
# if not res:
# return public.return_message(-1, 0, '接口请求失败')
# try:
# obj = json.loads(res)
# except Exception:
# return public.return_message(-1, 0, '响应解析失败')
# status = obj.get('status')
# success = bool(status)
# # 刷新软件列表状态,确保最新软件列表信息获取
# public.flush_plugin_list()
# return public.return_message(0, 0, obj)
# except Exception:
# return public.return_message(-1, 0, '领取失败')
# ------------------------ 内部工具方法 ------------------------
def _get_three_days(self):
"""返回今日、昨日、前日的日期字符串(YYYY-MM-DD)"""
now = datetime.now()
today = now.strftime('%Y-%m-%d')
yesterday = (now - timedelta(days=1)).strftime('%Y-%m-%d')
day_before = (now - timedelta(days=2)).strftime('%Y-%m-%d')
return today, yesterday, day_before
def _get_7day_dates(self):
"""返回近7天日期列表(包含今日), 每项格式YYYY-MM-DD"""
base = datetime.now()
dates = []
for i in range(6, -1, -1):
dates.append((base - timedelta(days=i)).strftime('%Y-%m-%d'))
return dates
def _get_7day_range(self):
"""返回近7天范围的字典: {start_date, end_date}"""
base = datetime.now()
start_date = (base - timedelta(days=6)).strftime('%Y-%m-%d')
end_date = base.strftime('%Y-%m-%d')
return {'start_date': start_date, 'end_date': end_date}
def _validate_site_name(self, site):
"""校验站点名,仅允许字母、数字、点、短横线、下划线,长度<=128"""
if not isinstance(site, str):
return False
if len(site) == 0 or len(site) > 128:
return False
return re.match(r'^[A-Za-z0-9._-]+$', site) is not None
def _safe_read_json(self, path):
"""安全读取JSON文件失败返回None"""
try:
if not os.path.exists(path):
return None
body = public.readFile(path)
if not body:
return None
return json.loads(body)
except Exception:
return None
def _ensure_metrics(self, data):
"""规范化指标字典缺失字段按0处理类型转为int"""
keys = ['traffic', 'requests', 'ip', 'uv', 'pv']
result = {}
for k in keys:
try:
v = int((data or {}).get(k, 0)) if isinstance(data, dict) else 0
except Exception:
v = 0
result[k] = v
return result
def _humanize_bytes(self, n):
"""按1024换算返回人类可读格式"""
try:
n = int(n)
except Exception:
n = 0
units = ['B', 'KB', 'MB', 'GB', 'TB']
size = float(n)
idx = 0
while size >= 1024 and idx < len(units) - 1:
size /= 1024.0
idx += 1
# 保留两位小数
if idx == 0:
return f"{int(size)} {units[idx]}"
return f"{round(size, 2)} {units[idx]}"
def _format_daily_stat(self, metrics, include_compare=None):
"""将指标格式化为返回结构include_compare用于today对比昨日"""
m = self._ensure_metrics(metrics or {})
formatted = {
'traffic_bytes': m['traffic'],
'traffic_human': self._humanize_bytes(m['traffic']),
'req_count': m['requests'],
'ip_count': m['ip'],
'uv': m['uv'],
'pv': m['pv']
}
if include_compare is not None:
formatted['compare_vs_yesterday'] = include_compare
return formatted
def _compute_compare(self, today_metrics, yesterday_metrics):
"""计算与昨日的对比返回各指标的abs/pct/trendpct保留两位小数"""
t = self._ensure_metrics(today_metrics or {})
y = self._ensure_metrics(yesterday_metrics or {})
result = {}
for src_k, out_k in [('traffic','traffic'), ('requests','req_count'), ('ip','ip_count'), ('uv','uv'), ('pv','pv')]:
abs_change = t[src_k] - y[src_k]
pct = 0.0
if y[src_k] > 0:
pct = round((abs_change / y[src_k]) * 100.0, 2)
else:
# 昨日为0无法计算百分比按规则返回0
pct = 0.0
trend = 'flat'
if abs_change > 0:
trend = 'up'
elif abs_change < 0:
trend = 'down'
result[out_k] = {'abs': abs_change, 'pct': pct, 'trend': trend}
return result
def _monitor_enabled(self):
"""检测是否启用监控报表数据源。存在 /www/server/panel/plugin/monitor 且配置中的 data_save_path 可用时返回 True"""
try:
if not os.path.exists('/www/server/panel/plugin/monitor'):
return False
db_path = self._get_monitor_db_path()
return bool(db_path and os.path.isdir(db_path))
except Exception as e:
return False
def _get_monitor_db_path(self):
"""读取监控报表配置,获取 data_save_path。结果缓存到实例属性以减少IO"""
try:
if hasattr(self, '_monitor_db_path') and self._monitor_db_path:
return self._monitor_db_path
conf_file = '/www/server/panel/plugin/monitor/monitor_data/config/config.json'
conf_data = None
try:
conf_str = public.readFile(conf_file)
conf_data = json.loads(conf_str) if conf_str else None
except Exception:
conf_data = None
db_path = None
if isinstance(conf_data, dict):
db_path = conf_data.get('data_save_path')
self._monitor_db_path = db_path
return db_path
except Exception:
return None
def _list_sites_monitor(self):
"""从监控报表数据目录枚举站点子目录(仅合法站点名且存在 request_total.db"""
sites = []
try:
base = self._get_monitor_db_path()
if not base or not os.path.isdir(base):
return sites
for name in os.listdir(base):
full = os.path.join(base, name)
db_file = os.path.join(full, 'request_total.db')
if os.path.isdir(full) and self._validate_site_name(name) and os.path.isfile(db_file):
sites.append(name)
except Exception:
pass
return sites
def _read_site_day_from_monitor(self, site, date_str):
"""从监控报表 request_total.db 读取单站某日指标异常或缺失返回0集"""
result = {'traffic': 0, 'requests': 0, 'ip': 0, 'uv': 0, 'pv': 0}
try:
base = self._get_monitor_db_path()
if not base:
return result
db_file = os.path.join(base, site, 'request_total.db')
if not os.path.isfile(db_file):
return result
# 日期转换为YYYYMMDD
ymd = date_str.replace('-', '')
ts = db.Sql()
ts._Sql__DB_FILE = db_file
fields = 'SUM(sent_bytes) as traffic, SUM(uv_number) as uv, SUM(ip_number) as ip, SUM(pv_number) as pv, SUM(request) as requests'
row = ts.table('request_total').where("date=?", (ymd,)).field(fields).find()
ts.close()
if isinstance(row, dict) and row:
for k in result.keys():
try:
result[k] = int(row.get(k, 0) or 0)
except Exception:
result[k] = 0
except Exception:
pass
return result
def _ensure_summary_dir(self):
"""确保SUMMARY_DIR存在"""
try:
if not os.path.isdir(self.SUMMARY_DIR):
os.makedirs(self.SUMMARY_DIR, exist_ok=True)
except Exception:
pass
def _safe_write_json_atomic(self, path, data):
"""原子写入JSON先写临时文件再替换为目标文件"""
try:
dir_name = os.path.dirname(path)
try:
os.makedirs(dir_name, exist_ok=True)
except Exception:
pass
base_name = os.path.basename(path)
tmp_path = os.path.join(dir_name, '.' + base_name + '.tmp')
with open(tmp_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False)
os.replace(tmp_path, path)
return True
except Exception:
try:
# 回滚临时文件
if 'tmp_path' in locals() and os.path.exists(tmp_path):
os.remove(tmp_path)
except Exception:
pass
return False
def _list_sites(self):
"""枚举站点列表优先使用监控报表数据源否则遍历TOTAL_DIR下的目录"""
try:
if self._monitor_enabled():
return self._list_sites_monitor()
except Exception:
# 监控数据源异常时回退旧方式
pass
sites = []
base = self.TOTAL_DIR
try:
if not os.path.exists(base):
return sites
for name in os.listdir(base):
full = os.path.join(base, name)
if os.path.isdir(full) and self._validate_site_name(name):
sites.append(name)
except Exception:
pass
return sites
def _site_day_path(self, site, date_str):
"""拼接单站点某日JSON路径/total/{site}/{YYYY-MM-DD}.json"""
return os.path.join(self.TOTAL_DIR, site, f"{date_str}.json")
def _load_summary_for_date(self, date_str):
"""读取全站汇总持久文件 /summary/{YYYY-MM-DD}.json返回指标或None"""
path = os.path.join(self.SUMMARY_DIR, f"{date_str}.json")
data = self._safe_read_json(path)
if data is None:
return None
return self._ensure_metrics(data)
def _aggregate_all_sites_for_date(self, date_str):
"""聚合全站在某日的指标遍历站点逐一读取缺失文件按0处理"""
total = {'traffic': 0, 'requests': 0, 'ip': 0, 'uv': 0, 'pv': 0}
for site in self._list_sites():
m = self._aggregate_site_for_date(site, date_str)
for k in total.keys():
total[k] += m.get(k, 0)
return total
def _aggregate_site_for_date(self, site, date_str):
"""读取单站点某日指标优先监控报表DB失败回退旧文件缺失或异常返回全部0"""
# 优先从监控报表数据源读取
try:
if self._monitor_enabled():
return self._ensure_metrics(self._read_site_day_from_monitor(site, date_str))
except:
# 数据源异常时回退旧方式
pass
# 旧文件方式
path = self._site_day_path(site, date_str)
data = self._safe_read_json(path)
m = self._ensure_metrics(data or {})
return m
def _get_all_sites_total_for_date(self, date_str):
"""优先读取summary不存在则回退聚合原始站点文件"""
summary = self._load_summary_for_date(date_str)
if summary is not None:
return summary
return self._aggregate_all_sites_for_date(date_str)
def _get_site_total_for_date(self, site, date_str):
"""读取单站某日指标(直接读取原始文件)"""
return self._aggregate_site_for_date(site, date_str)
def _build_trend_7days_all(self):
"""构建全站近7天趋势points数组历史6天缺失则计算并写入缓存今日实时不写缓存"""
points = []
today = datetime.now().strftime('%Y-%m-%d')
for d in self._get_7day_dates():
if d == today:
# 今日实时统计跳过summary缓存
m = self._aggregate_all_sites_for_date(d)
else:
# 历史天优先读取summary缺失则实时计算并写入缓存
summary = self._load_summary_for_date(d)
if summary is None:
m = self._aggregate_all_sites_for_date(d)
# 写入SUMMARY_DIR/{YYYY-MM-DD}.json
try:
self._safe_write_json_atomic(os.path.join(self.SUMMARY_DIR, f"{d}.json"), self._ensure_metrics(m))
except Exception:
pass
else:
m = summary
points.append({
'date': d,
'traffic_bytes': m['traffic'],
'traffic_human': self._humanize_bytes(m['traffic']),
'req_count': m['requests'],
'ip_count': m['ip'],
'uv': m['uv'],
'pv': m['pv']
})
return points
def _build_trend_7days_site(self, site):
"""构建单站近7天趋势points数组历史天缺失则计算并写入 /total/{site}/{YYYY-MM-DD}.json今日实时不写缓存"""
points = []
today = datetime.now().strftime('%Y-%m-%d')
for d in self._get_7day_dates():
if d == today:
m = self._aggregate_site_for_date(site, d)
else:
path = self._site_day_path(site, d)
data = self._safe_read_json(path)
if data is None:
m = self._aggregate_site_for_date(site, d)
try:
self._safe_write_json_atomic(path, self._ensure_metrics(m))
except Exception:
pass
else:
m = self._ensure_metrics(data)
points.append({
'date': d,
'traffic_bytes': m['traffic'],
'traffic_human': self._humanize_bytes(m['traffic']),
'req_count': m['requests'],
'ip_count': m['ip'],
'uv': m['uv'],
'pv': m['pv']
})
return points
def _get_top_sites_for_date(self, date_str, metric='pv', order='asc', limit=5):
"""计算指定日期的站点当天排行"""
if metric not in ['traffic', 'req_count', 'ip_count', 'uv', 'pv']:
metric = 'pv'
if order not in ['asc', 'desc']:
order = 'asc'
result = []
for site in self._list_sites():
m = self._aggregate_site_for_date(site, date_str)
result.append({
'site': site,
'traffic_bytes': m['traffic'],
'traffic_human': self._humanize_bytes(m['traffic']),
'req_count': m['requests'],
'ip_count': m['ip'],
'uv': m['uv'],
'pv': m['pv']
})
# 映射排序字段
metric_alias = {'traffic': 'traffic_bytes', 'req_count': 'req_count', 'ip_count': 'ip_count', 'uv': 'uv', 'pv': 'pv'}
sort_key = metric_alias.get(metric, 'pv')
reverse = (order == 'desc')
result.sort(key=lambda x: int(x.get(sort_key, 0)), reverse=reverse)
if isinstance(limit, int):
if limit <= 0:
limit = 5
if limit > 50:
limit = 50
else:
limit = 5
return result[:limit]
def _get_top_sites_last_7_days(self, metric='pv', order='asc', limit=5):
"""计算近7天累计的站点排行固定metric=pvorder=asc"""
if metric not in ['traffic', 'req_count', 'ip_count', 'uv', 'pv']:
metric = 'pv'
if order not in ['asc', 'desc']:
order = 'asc'
dates = self._get_7day_dates()
result = []
for site in self._list_sites():
agg = {'traffic': 0, 'requests': 0, 'ip': 0, 'uv': 0, 'pv': 0}
for d in dates:
m = self._aggregate_site_for_date(site, d)
for k in agg.keys():
agg[k] += m.get(k, 0)
result.append({
'site': site,
'traffic_bytes': agg['traffic'],
'traffic_human': self._humanize_bytes(agg['traffic']),
'req_count': agg['requests'],
'ip_count': agg['ip'],
'uv': agg['uv'],
'pv': agg['pv']
})
# 排序字段映射
metric_alias = {'traffic': 'traffic_bytes', 'req_count': 'req_count', 'ip_count': 'ip_count', 'uv': 'uv', 'pv': 'pv'}
sort_key = metric_alias.get(metric, 'pv')
reverse = (order == 'desc')
result.sort(key=lambda x: int(x.get(sort_key, 0)), reverse=reverse)
if isinstance(limit, int):
if limit <= 0:
limit = 5
if limit > 50:
limit = 50
else:
limit = 5
return result[:limit]
def _get_rec_status(self):
try:
u = public.get_user_info()
if not isinstance(u, dict):
return False
serverid = u.get('serverid')
access_key = u.get('access_key')
uid = u.get('uid')
if not serverid or not access_key or uid is None:
return False
payload = {
'serverid': serverid,
'access_key': access_key,
'uid': uid,
'activity_id': 44
}
url = 'https://www.yakpanel.com/newapi/activity/panelapi/get_free_activity_info'
res = public.httpPost(url, payload)
if not res:
return False
try:
obj = json.loads(res)
except Exception:
return False
data = obj.get('data')
if isinstance(data, dict):
s = data.get('status')
detail = data.get('detail')
buy_status = None
if isinstance(detail, list) and len(detail) > 0:
buy_status = detail[0].get('buy_status')
elif isinstance(detail, dict):
buy_status = detail.get('buy_status')
return (s == 1 or str(s) == '1') and (buy_status == 1 or str(buy_status) == '1')
if isinstance(data, list) and len(data) > 0:
item = data[0]
s = item.get('status')
detail = item.get('detail')
buy_status = None
if isinstance(detail, list) and len(detail) > 0:
buy_status = detail[0].get('buy_status')
elif isinstance(detail, dict):
buy_status = detail.get('buy_status')
return (s == 1 or str(s) == '1') and (buy_status == 1 or str(buy_status) == '1')
return False
except Exception:
return False
def _get_rec_status_detail(self):
try:
u = public.get_user_info()
if not isinstance(u, dict):
return False, None
serverid = u.get('serverid')
access_key = u.get('access_key')
uid = u.get('uid')
if not serverid or not access_key or uid is None:
return False, None
payload = {
'serverid': serverid,
'access_key': access_key,
'uid': uid,
'activity_id': 44
}
url = 'https://www.yakpanel.com/newapi/activity/panelapi/get_free_activity_info'
res = public.httpPost(url, payload)
if not res:
return False, None
try:
obj = json.loads(res)
except Exception:
return False, None
data = obj.get('data')
detail_id = None
if isinstance(data, dict):
s = data.get('status')
detail = data.get('detail')
buy_status = None
if isinstance(detail, list) and len(detail) > 0:
buy_status = detail[0].get('buy_status')
detail_id = detail[0].get('id')
elif isinstance(detail, dict):
buy_status = detail.get('buy_status')
detail_id = detail.get('id')
return (s == 1 or str(s) == '1') and (buy_status == 1 or str(buy_status) == '1'), detail_id
if isinstance(data, list) and len(data) > 0:
item = data[0]
s = item.get('status')
detail = item.get('detail')
buy_status = None
if isinstance(detail, list) and len(detail) > 0:
buy_status = detail[0].get('buy_status')
detail_id = detail[0].get('id')
elif isinstance(detail, dict):
buy_status = detail.get('buy_status')
detail_id = detail.get('id')
return (s == 1 or str(s) == '1') and (buy_status == 1 or str(buy_status) == '1'), detail_id
return False, None
except Exception:
return False, None
def _check_config(self):
"""
@description 检查config配置是否需要更新,修复统计数量不显示问题
@return None
"""
header_file = "{}/data/table_header_conf.json".format(public.get_panel_path())
try:
if os.path.exists(header_file):
raw = public.readFile(header_file)
file_data = json.loads(raw)
if isinstance(file_data, dict):
updated = False
val = file_data.get("phpTableColumn", '')
if val:
try:
cols = json.loads(val) or []
has_day = False
for c in cols:
if c.get("label") == "daily flow":
has_day = True
if c.get("isCustom") is not True:
c["isCustom"] = True
if c.get("isLtd") is not True:
c["isLtd"] = True
break
if not has_day:
cols.append({"label": "daily flow", "width": 80, "isCustom": True, "isLtd": True})
file_data["phpTableColumn"] = json.dumps(cols)
updated = True
except Exception:
pass
if updated:
public.writeFile(header_file, json.dumps(file_data))
except Exception:
pass

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,697 @@
4wZIF38E5nxrLYkW2uUR7d3iaGQLSTjR4DuJynwT7k0=
XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q=
NF4oGUk70PXEJoCfkU1k+A==
XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q=
n+0ptngHIPIjFuMNQ53bfjShEzdRevWRII7gkpUCnnv/UrDRLzI6EacRbZ9rtF7FvPYjMlAfHWb/nGHM9wcG6+aR/tksDMHut+L69WH8RFc=
XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q=
PEKPgJeDDCLnL9UcS39EYZOZmcluSt+RuygO2AupCV4=
XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAa23LCoGr2YlDYZYC1tcZk6mnFUnSSAMkOAkjmcftNoKhCg3JPMIJRBsMXbngZMA3Q=
1u+XjG/2+GSQRv6EzCaWRQ==
XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAbFx1CPChpgx6xlsqPUrTBV
Z71ucSR75ppLp8TcGPXjF3pMbrAmHfUi73meeu5lk5g=
XqBLRXNa/0V7439P+rc6hEwNOx43wLjN61FZ6E2obAbFx1CPChpgx6xlsqPUrTBV
ysVvGAkjYd+CMvqMgwDSXLWjcMyzhbnUJllEs/XAJUc=
dTLQ8Wr3wPn7w6pte1B1YA==
piG6BsMF31u4R4iA6R4SRA==
SbQQ5SqO9QrBwZ9ObpMYLw==
wiYVtfO/yzajW1Tv5z7hyA==
e+1YSk47tjYGkSkbGftbZE2uYCN5IbNhy3jmrXvEvpA=
1u+XjG/2+GSQRv6EzCaWRQ==
XIfdJ79nObMM+vyAmKbTmw==
bbgfjwWjzPNOxnsxTb5hzQ==
NyzlU2YOTFNZFPd7RK11YaWJrITXEmKGOKhVLomHOc20dOocSxleZ0AROzWQsbmh
NKJqjhkvgNdUxIywfoLwoQ+rWX4Ub5rsDIB2VkgmsHbQZuxN+TYMgAuTxJq+YliuIqYyVFQzgzZmvrQpFRfdgg==
KRD7rDXR1lpP6RV4MkrPcG28zhBn0vpFdthRmDq9HnH7Fw5b5pc8ohsInWTJA3Bw+h6qJvktai+DZvzQ33OvPA==
1u+XjG/2+GSQRv6EzCaWRQ==
1u+XjG/2+GSQRv6EzCaWRQ==
1POn+WtE+lgXD3ffcoL21lvcxUuxtU7UEzaHRLypkEk=
dOwNHIv3VHyb+LXIiMhwhoNJo9GQoWdlxj44kIhx6Hp82Kdhlr8gY2+EdvH4A5GLyunyBppgYfFAecDj13CWYA==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknRXNf+EPwleJNY1jfx8qILnkV2USPGiWudZwlPY4n3oQ==
1u+XjG/2+GSQRv6EzCaWRQ==
/dMCrtsSS3AVo9f6yz5/6W682xaNsg6HJuCqCuwKk/0=
rAmx4p+JZV3M+BRHZb295XUSu2ydOkZmyVfYHgJEFr6pg8rmusZs+IDM4uRAMobX2AfudLBLSPmuRpQF4T5VAKRvfeoURZQ1xEjbrqOIQpoPiEQWE/XxPN5p2F97G+wQ
1u+XjG/2+GSQRv6EzCaWRQ==
Cqk96pIsiK9Huw0DNmmBVCTmzTTpp6oO8X20KVLBFbI=
9GxZpCRwMRDPejWR2Vvf+LKn0tNtFKp8Eh2tnr4Da9U=
wgR07xfoapmx6eEnFHXXYrIeMJaP3Lg2M3zaGXD/L9gjQJppw0Kdfu9YroE8LgSjx3OthaS5h4DbYAtc65wP0Q==
xWoGNWjKGPfI4gq8aHoTfBq1oaYa956TFzYuHTbqc+m/yYGgrco00Chzgj8jXX13K8mLh+/6MvqvJUOejPajKA==
1u+XjG/2+GSQRv6EzCaWRQ==
+1OulhOVgLGZsanMCIMHTov3J4R+Y/qOR1atWG/VUPMrqBDDVvYwgG74FwIEbUmH
wgR07xfoapmx6eEnFHXXYo0pRHKIKLUse0GXAaX9haXCB+q3nG7WybPngoy25/lr
O3CUgrw2GJfB+mDjH5+NdnceC2jE7ElwqGvUxIcW3ioxHSZL5I8DFWp0U9QXyZFCJEIa4bUmblNphufxI1RnSA==
VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjakEuF8eNiysMFwArESQEwY
VmVrGQo2zRokW/ZuO9bN6xqOsfK2LZ5KdaTaboUxT+J9crK/OkxM3Cj8QV4T4xVHdhEAJicef5dNzYYGTNmijMbUMqg0Qdml7huVnL95ngY=
sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q=
VmVrGQo2zRokW/ZuO9bN6+8Z7TpabL3QP28zOzKbdjarRfvLG/H9rGMUHrshOkfFMP/VpuSx1UKdq74H98nb16vzDG30yJp2flJ85R7kiKjdYt3OAAWEEUxVb/l0+Vql
1u+XjG/2+GSQRv6EzCaWRQ==
J2qwAW0nzQmbo9HpSOVZlqBGxoFlarg7r90Pqb8pOp0=
1u+XjG/2+GSQRv6EzCaWRQ==
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
+Frj5gqpvxE6m3yWzQ1UlTIgSgRpVewwUpyndC4ejPs=
1O9qhjCr7g42w74sxW8Jlv/qDVNMbtBQQRaVFyF+WSiIU5hjbnf46ZqeQa2eiuyJUliAXdRmA7jeXoq7Zvzsug==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknRXNf+EPwleJNY1jfx8qIL47rDksm6lN7pqbuOzxD+Rw==
HZgQ37DasMTj06jDF6JhZeOucr+OS/yts2fdjXQ7Dm+4vRt5IE1mD34qMjUvwdSpTUFb+HQxnDeJ2haDwxBEfw==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmQ2DsRtKL7gFMZzQhcxfIn9vv7elCYoxsXqtak4E/tNQ==
wgR07xfoapmx6eEnFHXXYtX+pH+0NWr1LYbDUaw1pomL5U7cS0NAfF3rE9bUtpXl5cYry5RpCH7tkxjCcl+JBIsfkiUE3HU5Jsl3yt70u28=
L0eUthVnpkGsmKFAX6d+uBWSR38I0ooDiDs76zI3rHk=
1u+XjG/2+GSQRv6EzCaWRQ==
hCTaGH97j27PDhFRKBPIW1mpxeh+5/xXma/dtrv0fcg=
1u+XjG/2+GSQRv6EzCaWRQ==
b4OJVZe8QyIpjuTpKXDL9A==
k1ktu/l6HFULD7Vyr8Cv0NTQ7W8T66w67/IAnPs1BphbcAqgtYD23/UPxAUUoOX3zHiZvFDvN79q8xGwTMxzD2H7sGNp8TutRAPVqCo4E6o=
mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU=
k1ktu/l6HFULD7Vyr8Cv0M4/SSQxyWRs7AonOXcLu6M=
lNAImT/ajyr0UfekriSt9zSq0YdZAJlybeil//6Le5Lk+VqsbLPic0TZyMhSi1i4
9uZN8r0CKVAbfzGtFDuvLzjQrmTpqtae6m5049udhE7jeDlzSXSQQb6RLWBeXr0h
lNbaHNaSPRfUV3Bq+5wk6/o/pGhUxfVnG6Vdh8vueai6mE2PiMdE0bc8q4jeGJk5
VmVrGQo2zRokW/ZuO9bN6wmr39mr/nsb2Dav+lIH7B04mbHZv7nimwjnfT2u87IJz/Cm6ht29LQKMvtjjf+B66QOHjnv2exZ+gY1I9sna3E=
VmVrGQo2zRokW/ZuO9bN6zpu8f8CSK4jKnbU3ZGJmtvI3LMBtK4e0vlEt/NRBmS6
VmVrGQo2zRokW/ZuO9bN68oOr6KTHQoLVUXKZT/zSoBoqKYJ9J0V8LlHuynr1gG3
VmVrGQo2zRokW/ZuO9bN64iIPtwqqzY7IGXnqm0yAg890VkNwZCVekNfOG742tBCDsTNSICGfsHcEmavjt8ryQ==
VmVrGQo2zRokW/ZuO9bN6/DCJtLswi79iRP2VS1Cl7lfMksK9QvQ1cbzmlzMcErh
VmVrGQo2zRokW/ZuO9bN67Cw9WPgw66+cjpw/Ea5bB7otkixB28WuBR7LhKcbZ+r
VmVrGQo2zRokW/ZuO9bN619j4sjPeFT5nbRqDaMF1SyRCEOykS6VtvIiINHSVF6d
VmVrGQo2zRokW/ZuO9bN6+ggYawrMTekjFNmeq2lamXS1PinPTjUn3ObpK94+9cw
VmVrGQo2zRokW/ZuO9bN61dj0GoH2G1Dp6ZIVlQ2vt0=
VmVrGQo2zRokW/ZuO9bN64Mo+xDfj7h1iI635dygp4XcwMixUpSGp93xEK87JQBN
VmVrGQo2zRokW/ZuO9bN64iIPtwqqzY7IGXnqm0yAg890VkNwZCVekNfOG742tBCy+Z23+N8lT5zKrFfykLJXw==
VmVrGQo2zRokW/ZuO9bN61dj0GoH2G1Dp6ZIVlQ2vt0=
YH231WTDnzQG3bFilBiqIg==
1u+XjG/2+GSQRv6EzCaWRQ==
b4OJVZe8QyIpjuTpKXDL9A==
0snEhJ9RKaTb75xc3VQkty1aZuNySWxV7zy7TvMj3VELunqJJCu6WbOmoTqe9fBTss+PuRF0k3VMAy77nLPDRKCNrghLXLD+9ZRohmhwvHw=
mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU=
0snEhJ9RKaTb75xc3VQkt+QytQcTDkSIVmyzyDgzN38=
1u+XjG/2+GSQRv6EzCaWRQ==
qEXluMqy4s3YaMlhK+yN2FagSHb43j5sbjmAKOncUT9h+EXTNgwO3qaX8l+Kgen0
lNAImT/ajyr0UfekriSt98+S7POF+Z6BRjAWZy5+byneV3Q5JJujzuhONtsDZg8T
9uZN8r0CKVAbfzGtFDuvLzBJvYBsQoZdglGiULRh5LkQepPU8A5qwSrOLluzUtVy8JAf3NUhjARoB9em7CKotw==
lNbaHNaSPRfUV3Bq+5wk6/o/pGhUxfVnG6Vdh8vueai6mE2PiMdE0bc8q4jeGJk5
VmVrGQo2zRokW/ZuO9bN6wmr39mr/nsb2Dav+lIH7B04mbHZv7nimwjnfT2u87IJz/Cm6ht29LQKMvtjjf+B66QOHjnv2exZ+gY1I9sna3E=
VmVrGQo2zRokW/ZuO9bN64Pq6Hr0GQda+jfI3yI24le74i0xC0v4Da596cm0JfkE
VmVrGQo2zRokW/ZuO9bN67jx0a4m693Et0R92z2wvL71x/3S2aSF/NvGCgu6CHqf
VmVrGQo2zRokW/ZuO9bN68oOr6KTHQoLVUXKZT/zSoBoqKYJ9J0V8LlHuynr1gG3
VmVrGQo2zRokW/ZuO9bN64iIPtwqqzY7IGXnqm0yAg890VkNwZCVekNfOG742tBCDsTNSICGfsHcEmavjt8ryQ==
VmVrGQo2zRokW/ZuO9bN6/DCJtLswi79iRP2VS1Cl7lfMksK9QvQ1cbzmlzMcErh
VmVrGQo2zRokW/ZuO9bN67Cw9WPgw66+cjpw/Ea5bB7otkixB28WuBR7LhKcbZ+r
VmVrGQo2zRokW/ZuO9bN619j4sjPeFT5nbRqDaMF1SyRCEOykS6VtvIiINHSVF6d
VmVrGQo2zRokW/ZuO9bN6+ggYawrMTekjFNmeq2lamXS1PinPTjUn3ObpK94+9cw
VmVrGQo2zRokW/ZuO9bN61dj0GoH2G1Dp6ZIVlQ2vt0=
VmVrGQo2zRokW/ZuO9bN64Mo+xDfj7h1iI635dygp4XcwMixUpSGp93xEK87JQBN
VmVrGQo2zRokW/ZuO9bN64iIPtwqqzY7IGXnqm0yAg890VkNwZCVekNfOG742tBCDsTNSICGfsHcEmavjt8ryQ==
VmVrGQo2zRokW/ZuO9bN61gFm3fkb7F9QaTU95L/jPOa/fpyFMOpGyv47wlhPWUavkMZtX7vhm++qsgSrcsbMs7iYZPvqF57PAVFlPrQb9g=
VmVrGQo2zRokW/ZuO9bN61dj0GoH2G1Dp6ZIVlQ2vt0=
YH231WTDnzQG3bFilBiqIg==
1u+XjG/2+GSQRv6EzCaWRQ==
IhuisKim47k91RVt8z8qtPC1yXtxP4HpO+Fq1WLJQfIrs72H+rdxAJbYDuziNMhCDP4nqJoVpCdCT84Q09H2lGhMa2KtuWLGVujAUwS8tc0=
CX0zxV/Ac/FH6f+lPc6emlnfZJ8TnNl2PJfU8arp9tA=
CX0zxV/Ac/FH6f+lPc6emtaE/Z+HQ+edntL70zTF0MU=
1u+XjG/2+GSQRv6EzCaWRQ==
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
QRy2uI6M17xa8VECXPwQ/r3FvZ9uhLswudGkKrhamRBUed+4wvaGk+50+pqYUcvK
vwPJuanCw0GGKmHnTL06kqiRb2TJevIkneRS65wI1Gg=
M6K7zImQ7EejaudBMeyRrzUBkAth/QVYgGw+e7NSNq0ok1LuJoFoxPIRP/laAjHQ
Dlyx6ZDFdkn72YIxkC3bo6dRBka5m193wr4Tuji3xDfal4l1bn1HdgJU+c7TJ1Vg
VmVrGQo2zRokW/ZuO9bN66A2fToKVW3Kp87AR7xLfgU=
VmVrGQo2zRokW/ZuO9bN69a0VQFchDw5lCmzSL0TyRpYBO2LnIP1Dq6Tn849AbL8mrG/FghB2zsTPS+n/OODJzFvaZSpI2TVY4zI3RcRmUk=
VmVrGQo2zRokW/ZuO9bN65/ToZah2A1bCyMMmWoOHmwTDoouye0hw/WXVVasqBFLoh/FEdn1xkV5SIYhN6mUm5rSgxZdrKoNrvXutRDqIHY=
VmVrGQo2zRokW/ZuO9bN69kILXNpKBWZL9i8mpYJQQ8=
VmVrGQo2zRokW/ZuO9bN6w1Y5WxgJQ5MLDnvJCe9W01+kddoq5diIGZiCmLcwjNsGj/5PV83zGZZyBT+DL2LZ3LIzW5cj5kK3psaPepTpvXCc+DC0DY9z91U0kNTbcmP
VmVrGQo2zRokW/ZuO9bN6yxP4eEsZPOAIxRf5Vq6NdXCvDij9/Mz7hFjclWDpZcvlMUPpuqhPrhUyyRjD2+HpfnXP2EDVQQwTF1bXx077bw=
VmVrGQo2zRokW/ZuO9bN654Enj8yhWFV20SJ83piZWE=
VmVrGQo2zRokW/ZuO9bN689VNSLJl+o30OP6460WfudZdrYr3BFzOC6rBJ7pfcP2
+plE/1bhdo64kO07cLlUX9vuy3nOWCHRda7Edql0VQ4=
1u+XjG/2+GSQRv6EzCaWRQ==
H723/Fixv18fFechEvPfz+RShgw63yZpm0RRiruD6qgKiIksxbm4PQvdWh0n3Qv5
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
fg6BEyojOsebUV1Da0ek5HUdtpZqeBZhnz+XcTtBkgkzND/kRvI8T41ZtFR1F++bMFHpQKZGVIOzgelgaNXw0fIw4UTXnS4xgy4+N07DRSA=
nB7uUqFfRyFo2dz1p5PSrbBSwrXmx/ZlKzCQbe4NOoWD3zLEqpTC3naia1wn2HnJ
nB7uUqFfRyFo2dz1p5PSrVCidKjUrygAfYqtvGaCTx+P7CYvtc/HsI+QlD24z6T1BkgpveKIgrBCfXy8mi9tN3S73ZSEBsomyCpaYUenOzM=
AW6BcohkqRe+6MPCUtbq72rijnAQgwcBIRYlwd0OM3p1c3LrZX0CAPa3YjXFuLAqtq673F6jtVTw9IaFt9S2PBbfjX8+iwEzNVQ+AFpkuXA=
VmVrGQo2zRokW/ZuO9bN69h0gT5W9bsZSh/FPw+t8S7Xg3wbL376ONecW0BSarGt
M6K7zImQ7EejaudBMeyRr3H1QBxcRmFmUDWI5IEICl0=
n/Vg/an64od1JqfYD8zjtpmkT5XhnqiZIvQYECHVkbVtjF9uqp+q8tnLunH1B58zoVQtAiC8nqGnxZE4JD+cDA==
VmVrGQo2zRokW/ZuO9bN63P4gUZ6s3DvlDlNl4yHUxo=
aPdLEV6HY2SBr9CPW/3NlV6skNvI9JsIj9pOVhf2RvA=
1u+XjG/2+GSQRv6EzCaWRQ==
4TmkoDZIUXBDfgrEcjLrILoiV/r45eWI3EuL4iu8YCn8mmUZy1JG/0grjxu+JoNyI4MJoL1550P5cqm9YzkMmg==
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
QRy2uI6M17xa8VECXPwQ/msg81KEgiYBmI3HIABNuY6R+ST0p3U6tiMzZl8KC5E/+4LuyaVdKaDwd8xrlzplzA==
/0ULpLqgTvInFD0r5hHANlt/A4HN48BaWWKSY1CtQvDGiXT/SvUITbHNTthFbVxc
IpalQHG0U5xE1KnJ2rA3+zKtqFrPp3t19c9JGCjKU7o005Y2Y/EPTg/HqcBW9dPKmz/Fwy9EC/6B9qvDV4UqS6mdk3YE0NjY7aGYy8qDqg0=
B46DYlFQtDlulKRQCnJ3QcKt5tVSQirJA5+oY7kSjdHFB3yakZu8NqU5tBnxAUCR
ncUn2TP8ZQ4fZMBYk+CUrZu9buP2neyCDJBE1ohmVXDje+hgfrSkxiEVXRD5MMeB
GlJrPWeRtvgVw0Cl3yHj2DnrwYIJQs3/uKozgXNgdGiPoyv85RnxyG7THH1MXgLIEkGYD5JxVcpwFVCXpZX7mA==
jceVTICIbdeQFZqTJKGutfSrJlMev6knX9PMZKWQHjZP2zVkNQw5SLaJxnQB9+kv
1u+XjG/2+GSQRv6EzCaWRQ==
8AKpM+6i2gR98IaMzP+8eqqIPbXcMKDW+V81QFTAVtM=
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
EKWXQpq52OG0J2Wd0ZMK1fHl6IB0UXMhfHM8IMciaUE8Cuhs2Jaq+7F+INo0pV0b
tVC11ntb4xes47B6YgBZcZByliVS8Bcywk6SDhPLq9M=
U2Lm52IpUMUiy6pQmybrUNMsxQNvrzGUtIlYUGx/G+d+d11TtLhKhajQSiLMOGbM
1u+XjG/2+GSQRv6EzCaWRQ==
tVC11ntb4xes47B6YgBZcRoAGnZIKcHQ8817VE45RgE/szi0KWuuV0y+WiQG1xKnhxN36HfB+l+pBaungwquSjpKxsA0/x88FP41yUSvh5M=
dBHzFT+Qe5dH9WRInSu8jgBdjJ3d9vUVTbusOnLdjISlXe3y6Hp9jE1CEAXO1Vgo
lNbaHNaSPRfUV3Bq+5wk6635BWCQo9IrgUob1Zrag3U=
TNgWuy5WhP0PGg7S3lA/vmyECpd3KM52AqVa+3aAThk=
1u+XjG/2+GSQRv6EzCaWRQ==
JR/YEWislpM0oP8V4S7W/T3x/sEx/RV81gEcsXh5Vm4=
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
EKWXQpq52OG0J2Wd0ZMK1amw32WdMHSvsYBysyBVye7MBGZqqMlCnilH41Ppl5z5
hCTaGH97j27PDhFRKBPIW1mpxeh+5/xXma/dtrv0fcg=
b4OJVZe8QyIpjuTpKXDL9A==
lNbaHNaSPRfUV3Bq+5wk6zaqNMuN43kTxbH9sz7EO2ht3uTIAUE7QAf8gn2FiLoMM2qnty9PLRQax5H53+LRWsQG/ArUmkNEPQBKMtLhFic=
mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU=
s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU=
oacxXEoPih9NKsd5qduwcOQwetUrLBW9DCPWftWi5APwlnytsKeJdxzEsW4Uksji/RXLLSNM+uMNJD3dcCtnrQ==
L0eUthVnpkGsmKFAX6d+uIwh5dNhu3qfFXQw4bkQcLU=
TNgWuy5WhP0PGg7S3lA/vuqTekPEYUPjtE+v8avEsM0=
1u+XjG/2+GSQRv6EzCaWRQ==
5n6k7W+gw/zLAOXg7V7ef11JvrMmq8Abhcn0Lriux3c=
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
rqA8uevZ84obkmE4+PiALnegDM7v+GnS5uqMmEfzMznDMjjPN2InjgfCEjrrETPS
HXgXYcQ054mulb9vBwklSBYhmCzNXMpD7HETcV66cm3Ez8lsbeNFwRrL2pufjKIN
hCTaGH97j27PDhFRKBPIW/dJaxrfoD6RixVo3Rf5Yz3CGP+VwV5No6W32fmew1fv
/Apef+rUhQ+sRd878DPgvsqTHrbDs5B7KWyYuPbx/h60sw7UW87NeTBPLIIKfmyO
+7P0MlEc1eFk6hnR1wxxsLeMOEssU3yb0jKp/217OlU=
qMRWIbfylJbi8JdtlTKCc5qLCzoG+56ejKCriefYCzg=
VmVrGQo2zRokW/ZuO9bN617WCoPIsnyqpe8hS61d314=
VmVrGQo2zRokW/ZuO9bN66WOTbqs3JJ9oARgY2YlMq0=
sQfTf8f6jxyRSx9SNPNEtQ==
qMRWIbfylJbi8JdtlTKCc1faDhfd8MAGeioU59baa8k=
VmVrGQo2zRokW/ZuO9bN617WCoPIsnyqpe8hS61d314=
VmVrGQo2zRokW/ZuO9bN66WOTbqs3JJ9oARgY2YlMq0=
YH231WTDnzQG3bFilBiqIg==
661hZf7vhUQ+50okfwfTXw==
1u+XjG/2+GSQRv6EzCaWRQ==
uNSTKhXsc886vN3euLSVYkMvdkNBDE83aF6HgmNzzfm9nS8DOshApSz6BmwM4QxxEGBVCrw/4EI5+KcUH3As5A==
Dx/Tamsgq45f2G8qqPP8mCCoT8bqocyYm33wbOaP3Tg=
JpR/5cELI47P6HxjnatoQ2/atXOIHNcG1sc28haUJFZ1gZDGEl2o+OKEFUVLEwFaEzyLGFu5kNfMmEsr1svt1g==
r7wsdpnewjrvWfftu89eoYTwkMI5OWUW8b5cozC4vefJKFfxvzCdPhjxCkfhZhB4aI7EzyABwQuKXPBn2jYK3A==
pW8ou9f8OJbDl2h6RV18oo+Nb2xSCvRyGVnGpXJOZKFLJWxu4t/C+dMF19UdhbFV
pW8ou9f8OJbDl2h6RV18oikbwwEIAI/F+KSggaRJYmv/cWHXoGxXdp3crBfIEA5L
lNbaHNaSPRfUV3Bq+5wk63n5uJEj3P2III85nJ0uhYOX7Jw57j5UcSlV5FUAGDx8f48V0h/MPMwLz9LF6kln8w==
51UII3rZ87NZKsRBZ0eyLdVqoWpbfe8TmH2YJ9DWmQlS1xLSN4I2NnpSxLSgoi6ARf5O3cCN9gvq9c/fk2WhpQ==
51UII3rZ87NZKsRBZ0eyLdMEmNp2vC2WieoOz8+5XT1h8AmvAqKPSJXYghFKotIHwSJazFvEKrbDFNimm+5wCA==
TNgWuy5WhP0PGg7S3lA/vq/Pa4m0zdEJKukF/iQ8p7w=
1u+XjG/2+GSQRv6EzCaWRQ==
8AKpM+6i2gR98IaMzP+8eiursxazYPKLu6wgIOWPjLg=
kOT28QQt2GDU8VknV6OBKEjqdQ6KB8XgfYY8d9xdmSdjAru83zSuyfLg72ewneVt
oguKPwdzdIXoCdJnfha//pL2zpBMd1+gikCsUIV2A7Y=
WHkOzVx7seuLmxs5Hu+l/rPH5+mQcujSxbSmaq50dGDrgdFoJ8I/OP4q3nIZk9j6e/sIRs7MFvaiH8s1k9l4xA==
NhnIR3Ilo4H2su9/cTNo/Jz8iK0F0orMNzBf9My4ZxFxRp41ki11trhLUwQPw5UA2abvNkVk4cCi77fZpcH0AM7gHazPquEwSeick4HgGNc=
96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz6UPvO5bgA+DLcTH2CZLr/bGRj54IEJazMCgjM3+swz1g==
1u+XjG/2+GSQRv6EzCaWRQ==
VAMhP650VvSzUoGsza+Zzzu+y5xFCoLk4dxzpbTcSwM=
6we+bQ+BNhpl74ICwbzDDyJheDk8+zESwtN6nsP8zU7Uere9TaY0CfOByEukHqNf
c0jePRxtTVZYop75Q5JCFiIE3RJvFM05nYVUsJRwusjXXH+D9dAyYW92q67gBfn5
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLq5ZzPLUX/xqPUerYk2PoPswUHzqbrMkriRu9iUTYNkUQ=
c0jePRxtTVZYop75Q5JCFhNbnjqrU5CDzmRZm0r0Awym1X2wG8X/4uCaFLBbqGAp
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLq5ZzPLUX/xqPUerYk2PoPs8fM+dMsuU0wJOAXUgqZD09pxvBN50/ZgncRWUVKlprQ
c0jePRxtTVZYop75Q5JCFhNbnjqrU5CDzmRZm0r0AwwPUU7dHM1clC+rsS6Yiu9o
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLq5ZzPLUX/xqPUerYk2PoPszGlLmfF3+XBxrrFCfTO0u+lwudO575CdoR3BcS5rhnD
c0jePRxtTVZYop75Q5JCFhNbnjqrU5CDzmRZm0r0Awz+x7baA4CU9Vq58U1Mh/Bi
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXcMVI23sf4OEQYLlAO6/ObeURw20jcXuWd22HNjPFXSA==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklQ/NA0CCb5gYasRx4vE/jiwP1oPICdX16xr3p3prUwUnh9ppk6jJupRQKnfWADwuI=
B9nQgrssj+ws3f+4OSMxKmc+LynALf4SADHGSaLUuGY=
u0YJgTHLzbZHSJypIBwn/LJB/+7gHq/cLzGLRiGjmak2IsQN4SdsQxkVuqy6868E
cbvASHjpysrsjdY5RctXmOQD9rrCTb2ZxmWqcs5K2qSZ7dLEUX0XjSNDZ5yd5cTx8AH3snW3RGX7lCwH1f0D0g==
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLq1lfQhkMONeVXRmV7LJJvKul+Fy89aBAE/9tY8gTDXFzpUvLB4TFO7WEFIw0odvXb
cbvASHjpysrsjdY5RctXmOQD9rrCTb2ZxmWqcs5K2qTNDFwTtRmB79/9v3BVJsrHpdji1CVknwhFllSpjXF2lA==
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXcMVI23sf4OEQYLlAO6/ObeURw20jcXuWd22HNjPFXSA==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknjAU/EofwtdUePHnhs2gewQri/RccLzVJp08Zsrqre1shu8DNVPQEBXLS9qc4V6E0=
OE58XJkoqje9oPDfqi73ViDhb/MaA1CDRRN3hfs5qLcD7wPRDa7t67sgSM6GZud7iM7Q+yRzOZwxlLWELW/MJg==
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLq1lfQhkMONeVXRmV7LJJvKlg2pHEHdC2FnFJvgbKhNWegIP5NFaPmE64WTgBO0ycN
A69EZujp4WFkVkJizqN6C8VUkrxVow0gg23pw9cAA/uGssiFLD9jqO7nvqtN8xHKbEUH8Q6uj7cH8Ol7YnBkNw==
1S1xjYS0s+Ph1PyvECx3QHbY1OYa5gxYwaadGhIGNE4IFG/B7/pkR8wd8Pmtij+2zp1qIIbA4jXbHOMeTyXJWoDCmIqrdouDeBF95pQ4YIo=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpt9ortgN38QM9ut7GqkF6952rTeSJFUEYTgtdoJlNBjI=
1V2v8QerKOmubvSxgB4eTPvgycJaq1uM0FItIxQY+IBkx428stg/YVrimhpw15It
VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTlgv7Pbn3qn1rTK72IH5Ea
VmVrGQo2zRokW/ZuO9bN6xRyTbp7DqdRCfMT3PPA8YlDL0IuPjiay9076A6RftkxLnv1sxO4e5wt96hxsWEaz4bz3cyXhvh3Dxk+v8Yfelc=
HXgXYcQ054mulb9vBwklSBYhmCzNXMpD7HETcV66cm3Ez8lsbeNFwRrL2pufjKIN
fLETQ22HuDIh4ibW1C50WWehEWMVGJXX0CLf5TNvJo4Q5+7XSqZAhWnHUsbASo7V
fLETQ22HuDIh4ibW1C50WXEsbJxRZ4lXGXSSB22fG3I=
wgR07xfoapmx6eEnFHXXYo4se8cEG3Kpe+KryARC+58c1jICgdZ/yfiiPJOB8osn
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX0TLONUcGRzdgUZesuWNcR
VmVrGQo2zRokW/ZuO9bN6+u15s9HJHBIPZ+EJMD/NKzYtx5z2nANTnl7b4WNC9nIszUUAF2u6QjLYwXkHkGcYqKqXEXhdsB3+KXxMAaa/+M=
WHkOzVx7seuLmxs5Hu+l/vyGPI2ZUfF12Q0eaBVhpeoopSPGh26H6lFmBbaoJJwr
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXcMVI23sf4OEQYLlAO6/ObeURw20jcXuWd22HNjPFXSA==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hklt1+ZWs9i7/wHYqYPvKals5jrnT1gCW27PaXDJFdo5AIGAqA4bE2k2HJSMU/RDpaw=
WHkOzVx7seuLmxs5Hu+l/gzVZ0Va+DXFUK8nznHUhXhYsSTgQQX072ZHxHlXZn6E
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX0TLONUcGRzdgUZesuWNcR
VmVrGQo2zRokW/ZuO9bN6+u15s9HJHBIPZ+EJMD/NKzYtx5z2nANTnl7b4WNC9nI5t1B+HlkK75QMMuB3T2fEryfA0aaLxlmhGePr4B3TCc=
wgR07xfoapmx6eEnFHXXYhJE4xrEpFjyOULkKEdGQ6xpKpQ97D+w2oBDgGLxKzYi
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmXcMVI23sf4OEQYLlAO6/ObeURw20jcXuWd22HNjPFXSA==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hklt1+ZWs9i7/wHYqYPvKals5jrnT1gCW27PaXDJFdo5AIGAqA4bE2k2HJSMU/RDpaw=
hCTaGH97j27PDhFRKBPIWx5h0aqe91PCFXPRyuarZwHCnfY5zg3rAGF8JC1Rlqi7
1u+XjG/2+GSQRv6EzCaWRQ==
OE58XJkoqje9oPDfqi73VlGkW2sntmceo8fDxcMP5lY1ekhmQtt77mccgEqJdX7o
5p980mxWRpkxkeJsimzJ0c670/+1EUzG1KvIP4Osx0lqvbiasaqcKL7pqQoXRRlSmSgBIm75JI/P1rRgiosxmw==
VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTlgv7Pbn3qn1rTK72IH5Ea
VmVrGQo2zRokW/ZuO9bN60kPGAe8BYlpKmCcCBaEvbVM0wtnd/Sxa3UXu5M3ODaXwT+F3h/lqsCt7RwtEnQ+Juic62CWc/I0sBOjdt4E/FU=
lNbaHNaSPRfUV3Bq+5wk64nrImxso5dDuILvGI+MQwTmWuTQaQzUpeGGa/V25O8w
lNbaHNaSPRfUV3Bq+5wk6wD9WRwpEUG8eRNjfnAkH7n/D7vEeaS6J3HXXnWCNV4afdFt7wSC63jvA3Dmj7kOFiXPh8V4ZxzOGAZNxEO/GSw=
lNbaHNaSPRfUV3Bq+5wk6wD9WRwpEUG8eRNjfnAkH7mxlEizZI4FzeAXryBJu9K5U1bEK3vTRQwkjV/kSXPbLw==
VmVrGQo2zRokW/ZuO9bN68rCCyFZ0GefvEM2z0oNIJFc4wqFGELFP6LESyfNOCfv5kKX8qHPVm9l+0s8kRJrRg==
lNbaHNaSPRfUV3Bq+5wk6wD9WRwpEUG8eRNjfnAkH7n32akGeJETlKGDSs8LfYYC9O61WqHxRHNJwq7Sjf9EemFAkIFEWr7tHIeJwQnQvCo=
lNbaHNaSPRfUV3Bq+5wk6wD9WRwpEUG8eRNjfnAkH7n/F8iZ64HMLmMMgGltzdd5Mybak4VoNhIJ9JWGMsNufQ==
VmVrGQo2zRokW/ZuO9bN68rCCyFZ0GefvEM2z0oNIJFpouw5KukS9rQdg/yOLtUfQIhjL4gMIuurr0xfp6USoQ==
lNbaHNaSPRfUV3Bq+5wk6wD9WRwpEUG8eRNjfnAkH7ndT8Do9qwTTLaDEmGd5tQby5rX6DaA9VhhKXndgHL9JuvRDdXFPEMCn7QL0m3lUzs=
VmVrGQo2zRokW/ZuO9bN608deJSvexKXO21NHnmMGHeIg/pr3nDhgNXYDiErXtGU
lNbaHNaSPRfUV3Bq+5wk6xOlK22tFRCz/MBWHqTO5bcxlrGueZZq4wrMs8Z1lnfjDuzShbwQZHdqLJ+DV3ERdQ==
VmVrGQo2zRokW/ZuO9bN62ec9pTpN5Cg+2rTnrzF44sbSkSgWYcimLMO39fwdTM8QQ9HapIm3253ANyva+ZKow==
l2FJPs4YkAmmok1ulDRuSA==
lNbaHNaSPRfUV3Bq+5wk67RG2evr7TLF3SKM8U8p2AA=
VmVrGQo2zRokW/ZuO9bN6+GpeDJLU0SmT7L+/5sE5xIlwrT8TwQpkYYGHjbVkNftN1CKdBCjUzympMQ9RT/FEg==
VmVrGQo2zRokW/ZuO9bN60neO6DA37E8cryoS6/9NzyxLC2oEdN7DcJRwQhdCjJH
VmVrGQo2zRokW/ZuO9bN68oOr6KTHQoLVUXKZT/zSoBoqKYJ9J0V8LlHuynr1gG3
VmVrGQo2zRokW/ZuO9bN6yJjT4k1EBcR3P33HPsJKcS2iJiueG/E56TTg8JuY5PMBrWa6U4B5U8rW1KDAZ2JSficZo3wFKS2XvW6ui8Tv2Y=
VmVrGQo2zRokW/ZuO9bN6/DCJtLswi79iRP2VS1Cl7m3kLpB5lzqL2Me52OhtZ1zUFOJHRYhWSvYLeuSZixvqmuGQwZgeXJcumVSIEgd/00=
VmVrGQo2zRokW/ZuO9bN6xRgv05zRD9dGM5M32jg6eA41WZXP7dtRrdMXjEoH3lvGk8OfKeqpDa0bqO9TtO5SbDD1FpNmhqJhoAYwQK56Qs=
VmVrGQo2zRokW/ZuO9bN619j4sjPeFT5nbRqDaMF1SytlAuchmjuOqjKzKLlRlpKp0wX5I7ca4hs64AaenatopykN6zoa1vKVmy5GNym1kk=
VmVrGQo2zRokW/ZuO9bN68Ys5qSHlhgxubR3riCCkMH07t3rA9i/RBuH8FqAjmcn82Cl0Yv2F59R3Y2ngEly5Bn5i06KWEOErKyGGIaEm5s=
VmVrGQo2zRokW/ZuO9bN61dj0GoH2G1Dp6ZIVlQ2vt0=
VmVrGQo2zRokW/ZuO9bN64Mo+xDfj7h1iI635dygp4XcwMixUpSGp93xEK87JQBN
VmVrGQo2zRokW/ZuO9bN6yJjT4k1EBcR3P33HPsJKcRSOVZETH4NsaP+09MQKNzjBQ68t3nxeo4G6JqqxhrjboknRazz5h4TuwgKRRmKioM=
VmVrGQo2zRokW/ZuO9bN61dj0GoH2G1Dp6ZIVlQ2vt0=
YH231WTDnzQG3bFilBiqIg==
1u+XjG/2+GSQRv6EzCaWRQ==
OE58XJkoqje9oPDfqi73VnAo3wfSgmMs+Dj8jbP7qWFcxm5vY3PPNCW7jzGH1g0O
eV/v00/xYHddkCxXY1E1CutyiNhsUp5dQKzgD5wic7IgXr6vXqh+W6d4JHYxTx7KWitSUvhADyOfRWeCoy8QOA==
Dlyx6ZDFdkn72YIxkC3bo1MEi9hlWmv1Zzc0sVxBv1A=
VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTlgv7Pbn3qn1rTK72IH5Ea
VmVrGQo2zRokW/ZuO9bN64IIz+/BGgp+g6IZceFmjbE=
VmVrGQo2zRokW/ZuO9bN64N/ZpXjHM9K9N0f3E2rn5WmGCGuCqdlsHbQ2dtHALi514chJeA1Hx2w6TnYXI1BkHbq2OIhHdxnU+azwjwrhDl2N6HE5nK+JyS7Sogxob13fbD21rB84V/svtOsrrU1++ayKZo53F/FxktbspEFfFGeAkkO+6B3q7B5P3cOtlN9
VmVrGQo2zRokW/ZuO9bN66+yo8koaMFaeR72YfcN0MA=
1u+XjG/2+GSQRv6EzCaWRQ==
PWKGbNEPxxxpGpe8THO245iFXovVReeQ+N8g1T/vWbG7nH535jnCqkS519sVTR6d
VmVrGQo2zRokW/ZuO9bN6zupz0i5vSNSVG9TGAJDyeoLDnDUXJHlYRiV20eRKyAX7xIMzgD8k3m4OcgwCDXDKzd9OsJEnb+M92CH7qeRZ0rXTaTN2IFoZniVzx0rvmSkMgg0Am0BzBuKc2Oy4b+Fwz22Rz9N82eyhvd8XOJC5uz4LBqftuz0hWqSTaSElBKoG3MPG7CNEYazXn7ounswqMclIOlDexzhfuPj/JPJkMXV9/kKnrcBcKBfppT05VExwJcuZ3qAf0UMyLGABEPcBYMQ+R9r/5jle4KezOy9UQns1yuOUpszLU/F0HSuvqLHoTleWG+8yf0L4bezFqV+OnoLU3fsTnAAvz4eBfP5CpHNwhaX0L8GQhbbhSCir00QDirnaca7yfuKjeHl9v4mJ+L80X7e9b6rV3BwLAxEclLhmkJjIsXn2ELLz6LA4q1p4l/V0/X4ui7HcDSp8fXfidL8c7vRgYqyWhzzbR2fE3UHBxaDFnpSWp0+ZuXHvCMScaIyJgZnYb+DTCjFMFU6lb4b9VNKQb9QbnC9ujuFU+JAw6rjiLPeHDb8gjkeBiQ9ynw5AYK0ZKL8d0Egv5xzVyq9+JxlM2dkfo11gyetKX4=
VmVrGQo2zRokW/ZuO9bN6707T5hcoTPt8muDk6mBkfIb2fVyGf9Ofl629l94fe8y7TbM2fJP1K6AAnHO78QGE4QIfFGZ0iAOEsBOZhdfiig=
VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrSuY4P/AlnqUWfNJ3Nv+yymeIVnB1recLomFaZp8gbNfQ==
1u+XjG/2+GSQRv6EzCaWRQ==
W+5RMzryWgfLCHr0OO+ErInNFnurUcbPlrIppCTTbAXA12wyKUkFjBea/Qb61VZ3c05s9epd9IsaBOqriuZPqMonOEUobnVmGGPlR7/5sVE=
VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTlgv7Pbn3qn1rTK72IH5Ea
VmVrGQo2zRokW/ZuO9bN64IIz+/BGgp+g6IZceFmjbE=
VmVrGQo2zRokW/ZuO9bN60cD4yDgVtlJBXBhnVmR4KRv3Cr6w0tsMAgfEOMZqertu+3og0F/QbIVS5TWho6wQ7g1mzy1fG1ZJjmIEe5S5/XwGDw0UuD1NCUzL9LbH/Ai
VmVrGQo2zRokW/ZuO9bN66+yo8koaMFaeR72YfcN0MA=
1u+XjG/2+GSQRv6EzCaWRQ==
lOh2GtzHjjMM8E9J40AuOWo7iSH0P5z8Y9QjLJCJBGgafK4AuE7/cXHaYaFctwNo
VmVrGQo2zRokW/ZuO9bN68M/5U7Jf9mVZjM7SR3NTECPgWjfF+U65HeMorIolhvD4HPEv93D1akoDkrkvPrOY9QS2DPJBdmooxB2TGPQNT0=
VmVrGQo2zRokW/ZuO9bN64jhM4qvFQxrXhREkkL4evAvbPabM3xnVTII4I8povi4f3Pd2gWnwleD2BdzN0izmg==
UbJuac0dxLiN+5ocS3w2vb7nzZrB/7qqG45ll0i5qjk=
VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTlgv7Pbn3qn1rTK72IH5Ea
VmVrGQo2zRokW/ZuO9bN6z2QIHM+MwWG+DHtm57W1+SfMGavOQXjtK5E+Pd/RbfJnnyRuS5HM/BDbhDuGwNAThMAfBz6ti7yWZ76KqpxFpk=
lOh2GtzHjjMM8E9J40AuOWo7iSH0P5z8Y9QjLJCJBGgafK4AuE7/cXHaYaFctwNo
VmVrGQo2zRokW/ZuO9bN68M/5U7Jf9mVZjM7SR3NTECEaiFjmR/qWPRUlpmZbiW5VyN/W5PhHp8jokr2q+UyLUeUAIJ6zu+f1a6ekzajeN3jJrbDfSbX1Y+ZZXd3Q0JE
VmVrGQo2zRokW/ZuO9bN68AJK8ucG0UjIlJ8vTejx4/UjlGzME7k234YwzM/EMl+2fL+nZx4XxqxEr363b5fjg==
VmVrGQo2zRokW/ZuO9bN67wENRoFfl+EER/m7L/ET4GRAT4HyRemKVvrVKbyjVfM
VmVrGQo2zRokW/ZuO9bN62Gbiu8sHPTspuRWoP5/2uyG+jNo2Z12jeXvFZbcfbfDFcbvNH4DvbGdfzF2nAV12A==
UbJuac0dxLiN+5ocS3w2vb7nzZrB/7qqG45ll0i5qjk=
VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrTlgv7Pbn3qn1rTK72IH5Ea
VmVrGQo2zRokW/ZuO9bN6z2QIHM+MwWG+DHtm57W1+SfMGavOQXjtK5E+Pd/RbfJnnyRuS5HM/BDbhDuGwNAThMAfBz6ti7yWZ76KqpxFpk=
1u+XjG/2+GSQRv6EzCaWRQ==
uRzTWU2MnopGqqEdWeT6ZeDusW2quXQd3pL0k0Xg8wY=
1u+XjG/2+GSQRv6EzCaWRQ==
hCTaGH97j27PDhFRKBPIW4EZFPkfMH002JrNm9PLnXuwc9yq5Lyo6aOmZ30BZnoK
2+RceSNjD2iq0sRzepMysYKB2p+0quZ3k/r81UP7jdA=
0tTy9hW41S4+Kusr1EouU1D1oliZ7e6CIIanBdYINco=
QSlGrRvW/rNBDeF7we6iBWKk2BA8QQKFrP2zSqI9P7ki0jFsOC40u8XHDVJCMmICcxej7Emo/6XIHyFNtg2eKufmZGJET5cF7chd87WOItU=
VmVrGQo2zRokW/ZuO9bN64GHxlyw4JoKjlRq0kI99uVlNgUvBBu0glcGT+bS1WfEDCMuy8Daf9HtiO9MNlppARClW0pxHS+4u0haP8galI0=
IhuisKim47k91RVt8z8qtM11IfmUg3Vzb1vU4L+nUSEXI43SqtfmWpr/I8AYFS79jp/blpkCZsPn/8gvY1O0DDcOOIqpPruCuHCHNHbX95w=
96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz6MJouh6jubSHLlzeu4ZDhMi4c88PYp0hrZSfjI5JTCeSwrNjymmREnDUPzBiIcTHg=
1u+XjG/2+GSQRv6EzCaWRQ==
uC+DXmi0z9guz6E2K34tbxYHTBFHe6AQnlyDTD6Hc/rV77vRQxTn+MRGJaaqppIF
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
kOT28QQt2GDU8VknV6OBKCQUts5PU1sJ6OhmjdEHWvV7DuMTPnU4QUe9GhokxYr/
qEXluMqy4s3YaMlhK+yN2PBqpgcwSiqQwLqIpKZKB54=
HNr5/wGY+7coHgowTe94LmyrzabZ+F1uba2S8ZHow+auSBEdEXNeDwaLFHcP6wVoY1XpyD3B5G5cVIWyIhMziGO8FVB1Vl4mxAxFXsSIfZ4=
mqjkDRz7NSn5aKPgUezVq11iXRHQrp2tT15ujt/F0bYTQCYjcppgnuJEuiMNwNgr
NhnIR3Ilo4H2su9/cTNo/DyCETVXfy75d7o8helWnBsvvixqWmCON4WSy97dJwy2
1u+XjG/2+GSQRv6EzCaWRQ==
TZ7deu05xHGE9hF3JFWmFKNL8L7HO7Uej/uyjh+xMus=
YciZwvvDXocThCkMB+g25MvaKVnS5K98mkE7NC2a/WQ4mA3Ht59gE6D6i32RtvWR
qAbVEM8+SQkJ03a0z2JhDtNtV08C1I/YV/6wz4lDUuejaC1OXdVMCt4PD51qEytS
vQwaisdUFOt9b0SJYuH/nrHigrTUtycF35lf58xGUQo=
VmVrGQo2zRokW/ZuO9bN698k4+GGpSbXcov4Fqu/oAw0FvcgRKi/DMw/D9yaLkasmWznSNTKwz4/hi3mzIakiA==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HklcPuLfk0fyZt9CAMcgeEUFsN3OsL0zJLY8NmDJQSQhDA==
M0ZySqkmhuHCw6olbCKv9zQJzSQJo4Zslq06bXS41SQ=
VmVrGQo2zRokW/ZuO9bN69PvoA3R0Po8RscBiX748GvjYuB6m9pC5D+fF3ib+VKf
+plE/1bhdo64kO07cLlUX7e7q+kMaq7bcRu2iq9qqBQ=
1u+XjG/2+GSQRv6EzCaWRQ==
j/AwtlZD2qc0Bfi4crT5PVuymX2J11M8SjzJoWxpQSM=
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
rqA8uevZ84obkmE4+PiALqIGE496RRgzEOnxBnCJi5KM0A/RxMfdl5D+mQS+eMty
hCTaGH97j27PDhFRKBPIW/dJaxrfoD6RixVo3Rf5Yz3CGP+VwV5No6W32fmew1fv
qEXluMqy4s3YaMlhK+yN2FagSHb43j5sbjmAKOncUT9h+EXTNgwO3qaX8l+Kgen0
/Apef+rUhQ+sRd878DPgvuQmATc00QFbteWJRrJXqqc=
+7P0MlEc1eFk6hnR1wxxsLeMOEssU3yb0jKp/217OlU=
hC9Wqf+oYT8RvKOl1V/o2ht+Kp2nrdRz7LH4nKSmxsw=
qMRWIbfylJbi8JdtlTKCc5qLCzoG+56ejKCriefYCzg=
VmVrGQo2zRokW/ZuO9bN66WOTbqs3JJ9oARgY2YlMq0=
VmVrGQo2zRokW/ZuO9bN617WCoPIsnyqpe8hS61d314=
sQfTf8f6jxyRSx9SNPNEtQ==
qMRWIbfylJbi8JdtlTKCc1faDhfd8MAGeioU59baa8k=
VmVrGQo2zRokW/ZuO9bN66WOTbqs3JJ9oARgY2YlMq0=
VmVrGQo2zRokW/ZuO9bN617WCoPIsnyqpe8hS61d314=
YH231WTDnzQG3bFilBiqIg==
661hZf7vhUQ+50okfwfTXw==
1u+XjG/2+GSQRv6EzCaWRQ==
NrBMtcyWGtR1hVD5pwJYfwziCIADCHuKVoMfBVeB7td+eZjbZWU5lEr9RqqI2vmp
uNSTKhXsc886vN3euLSVYkMvdkNBDE83aF6HgmNzzfl+lr+dusojmKJFGktXhvrngS9Kwg2wwLx9IK9kj8NxAA==
Dx/Tamsgq45f2G8qqPP8mCCoT8bqocyYm33wbOaP3Tg=
JpR/5cELI47P6HxjnatoQ2/atXOIHNcG1sc28haUJFY4USrV8c8DFQmuaJj5pmjKoadTYNH/El5wiqoWDGDr6Q==
lNbaHNaSPRfUV3Bq+5wk63n5uJEj3P2III85nJ0uhYOX7Jw57j5UcSlV5FUAGDx8f48V0h/MPMwLz9LF6kln8w==
1u+XjG/2+GSQRv6EzCaWRQ==
51UII3rZ87NZKsRBZ0eyLdVqoWpbfe8TmH2YJ9DWmQlS1xLSN4I2NnpSxLSgoi6ARf5O3cCN9gvq9c/fk2WhpQ==
51UII3rZ87NZKsRBZ0eyLdMEmNp2vC2WieoOz8+5XT1h8AmvAqKPSJXYghFKotIHwSJazFvEKrbDFNimm+5wCA==
TNgWuy5WhP0PGg7S3lA/vq/Pa4m0zdEJKukF/iQ8p7w=
1u+XjG/2+GSQRv6EzCaWRQ==
3nekdX1X45iQPZ9gGedeIRFpzvW0t6AmXo0Vs+Q3B7MtIFmd1ITSQq54rNj3Of0V
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
W/E3KX5TvlXFgPkaqGzgjqZHeKuJLKPP6l4qtgNz7IY4hJWE8FmjSzvjmmmb5VunZ4QzK5iqrn9HNqRecsPrrjLC7uAEfwtdQjEdDm40Dwo=
iPZI+idezVAJw0Ib5lZU4dl17+hCGjC6tXjNver4UbLVz5ZhKILGgbDAaOsRj0c/
GIeBciyw9rxD3VOfe1EU6vwglDMZ6MEBP18g88btvtydgx9BEGtIPKmLOI6QmFbkxdYO7/HvVy6vSz6J5utDXg/muXopzjQJUZywMhtpdPA=
VmVrGQo2zRokW/ZuO9bN6xVGSYCDtM0pf92u8Yp9YqgrkVutZIWx3e8oFQ+V83/M
555vbfqS9x/Bki5IPjqgDMk1rrU5PWcetLJxQqKiztI=
YY5wEo94l9qfo5TPxQmUgiOUk9wZ4yEOytm43lMuFnP0mvaVswvUBe1Evg3XhOXV
VmVrGQo2zRokW/ZuO9bN6xeLX5vqx959YU/gZDt0YvZIPNF3mPlWCXYJIVjVWyc/N7aPs92P0AWzUUvFuNKpd1rTZ0TR2jK/sb0o+La8V7w=
VmVrGQo2zRokW/ZuO9bN67dFbrzCL8hImD2yMrdv4Vg=
iPZI+idezVAJw0Ib5lZU4dl17+hCGjC6tXjNver4UbLVz5ZhKILGgbDAaOsRj0c/
cHKRqjHJIG5yrQxjLveUB+BsKw4vJCwsahPFceqybLFw1oHD0QEOO5T2HuKqoFvf5JKkTScorvUimpe/w1iw3/20b2DhJrTx5Z/eZ8G1Pd8A+/nIh5G8dKiIOMDYEwkXv2bX/3bHTYJSjBrGPPQ2T1tEB8qyc93ELHuc8u/HjQX54E/ovWrEq5ukBa2/xfMqwB2Rn3KzuET5YnmuBYAyK2tysxpciP8LIikFg5VJLeZ0Hh9/dIZ1ahA7xKqyIZH+BO4KLnEoXtwZviiL88jehOElk1E2Xq7Ym8ZGcXfqBfk=
9HH0xFa0JPpRaw85JsXnfXwg8i96ZpFEflcqyM3HGgSPSZ2dtq3IxwdeQSueeE4yRiNtorfxbpEMEQJLb1pr9A==
555vbfqS9x/Bki5IPjqgDMk1rrU5PWcetLJxQqKiztI=
YY5wEo94l9qfo5TPxQmUgiOUk9wZ4yEOytm43lMuFnP0mvaVswvUBe1Evg3XhOXV
VmVrGQo2zRokW/ZuO9bN6xeLX5vqx959YU/gZDt0YvZIPNF3mPlWCXYJIVjVWyc/N7aPs92P0AWzUUvFuNKpd1rTZ0TR2jK/sb0o+La8V7w=
VmVrGQo2zRokW/ZuO9bN67dFbrzCL8hImD2yMrdv4Vg=
PAoN3u73Z3nXo35rPzNO33JGqzHB3DHCHZe0q2yDue7WFNUAgfdpyjJARPhEvbbL
guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE=
1u+XjG/2+GSQRv6EzCaWRQ==
Z02eW0OyMdUTGgS/rY5UtxvNt0bONzQv0Ne5fwFyOPvckIt7Mxia2dqwynmp7umh
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
3mFSQTn132ru23T3/05U2VtbqO/J7l6+ra/ix/dBlb4tFyl4q2VRrR7kEYMUqY32jfvvBlqKgjBPSVLN+fFOQE6uwzVJbbAN5c5vub7z7MA=
iPZI+idezVAJw0Ib5lZU4dl17+hCGjC6tXjNver4UbLVz5ZhKILGgbDAaOsRj0c/
GIeBciyw9rxD3VOfe1EU6vwglDMZ6MEBP18g88btvtydgx9BEGtIPKmLOI6QmFbkxdYO7/HvVy6vSz6J5utDXg/muXopzjQJUZywMhtpdPA=
VmVrGQo2zRokW/ZuO9bN6xVGSYCDtM0pf92u8Yp9YqgrkVutZIWx3e8oFQ+V83/M
555vbfqS9x/Bki5IPjqgDMk1rrU5PWcetLJxQqKiztI=
YY5wEo94l9qfo5TPxQmUgiOUk9wZ4yEOytm43lMuFnP0mvaVswvUBe1Evg3XhOXV
VmVrGQo2zRokW/ZuO9bN6/+//o1VxepUuVN4dtwtzUt1O4qqUXiJ+PucnHjF/NzPsClK+zrktiTn+Ec+l1k8pohfVzCbgFrhTHJ0VGrqs4Y=
VmVrGQo2zRokW/ZuO9bN67dFbrzCL8hImD2yMrdv4Vg=
iPZI+idezVAJw0Ib5lZU4dl17+hCGjC6tXjNver4UbLVz5ZhKILGgbDAaOsRj0c/
cHKRqjHJIG5yrQxjLveUB9ExfFoUGJtN5RuV80lRdDSmDk68awlmo7TMrBJtA4GHSQ9ZM94NKEG1NkhDJownhQ2r+B6ZIf0BzeTxSESFjTE=
VmVrGQo2zRokW/ZuO9bN6xVGSYCDtM0pf92u8Yp9YqgrkVutZIWx3e8oFQ+V83/M
555vbfqS9x/Bki5IPjqgDMk1rrU5PWcetLJxQqKiztI=
YY5wEo94l9qfo5TPxQmUgiOUk9wZ4yEOytm43lMuFnP0mvaVswvUBe1Evg3XhOXV
VmVrGQo2zRokW/ZuO9bN6/+//o1VxepUuVN4dtwtzUt1O4qqUXiJ+PucnHjF/NzPsClK+zrktiTn+Ec+l1k8pohfVzCbgFrhTHJ0VGrqs4Y=
VmVrGQo2zRokW/ZuO9bN67dFbrzCL8hImD2yMrdv4Vg=
PAoN3u73Z3nXo35rPzNO33JGqzHB3DHCHZe0q2yDue7WFNUAgfdpyjJARPhEvbbL
guSZID0bFQuDFoWO2uxAJoOpitq6s6c9ladjgxAHqGE=
1u+XjG/2+GSQRv6EzCaWRQ==
NuiaTGqwOgT56I/A6JuW+OYXU3qy8sRWssvgEhRPgEg=
sXXQWFPsb9cQdoRNLvTIV8rn2NRc5Be1ATIx2zUeuZqsOYDCo+7iBPhL/G74PPQT
c0jePRxtTVZYop75Q5JCFggVGiy5K37SY6O292Ia4MPkJnXEVXE/4VzqWDalKAbQ
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLq5ZzPLUX/xqPUerYk2PoPsymFnJ5By9OX6QOc1ugur9I=
1u+XjG/2+GSQRv6EzCaWRQ==
GtEk0s5RgbBW28KN6lhHJPglIAC2uEFNpbOm1inM5bU=
b4OJVZe8QyIpjuTpKXDL9A==
84Vymj90Wzn5yYuvz1pUuANPeEDUN+p9ZnLSm6zv1bU=
VmVrGQo2zRokW/ZuO9bN670H5P+s0tiwdJfbGZCx661rd6moQu3BvusznuCG0tD/
84Vymj90Wzn5yYuvz1pUuFE+OEJSVzvJho5ZjQiIPBe4Jo8zq8jO8peaF2StFqJO
VmVrGQo2zRokW/ZuO9bN670H5P+s0tiwdJfbGZCx663i8WbYWNuKhaXf+ufb67XdedTrYd6XbXJ4R6divvitmA==
mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU=
s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU=
1u+XjG/2+GSQRv6EzCaWRQ==
xvhr5kRQI+GM++iQkYI5iZR7ekEg1I0ti6W1CX4my1o=
i03pkbdctj+nAGBFpRXJf4F1uccpkrAJ6vfCX+MrMLo=
ZBEun/friv7tm8hfXQraKbzUFK6ic1paL3nAvarxZME1vwjFDPMgwxS15zNwFDB7
1V2v8QerKOmubvSxgB4eTBXpUZR6PqydO8mMhIOfyKFGthL4I2hlbLeW2mVUWO0YoHQS7Cw1T4h9CJtczURt+w==
VmVrGQo2zRokW/ZuO9bN6zM/X2hHBCd+n2OGnnATmrSuY4P/AlnqUWfNJ3Nv+yym8gDrRRn88+E97SwP9crSGg==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkljVbRSmRphUPMLwP2qafpDf0RBDjAildUzjjnayI8DL7HlvMSlZFKOqgTdesXR7sc=
1u+XjG/2+GSQRv6EzCaWRQ==
W+5RMzryWgfLCHr0OO+ErKxDL7PCXIjhB4TInB8eVS9O5hZl9zfsNzlK9ihwJvwAWqnaaomc3arDNjrzacptHg==
VmVrGQo2zRokW/ZuO9bN6w7q5JOrUTgpjW8N+SP2Atc7he+WeQiHTFqNuIlGY0i4THU1qwL26q1GrHcLZME4I/17G4fCcBY2RQ1y+CHrq3Q=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpEMPvuOg4yWJa5Si36Ytvw5qEGRH2b9ICWRSmYmVtGvQ=
VmVrGQo2zRokW/ZuO9bN60RwCdNuq+t1770A64ug2G/c85wRX5QkmJMqMmYnxhBYkD7eiGF9ZH8bGFOtAVcsnQ==
VmVrGQo2zRokW/ZuO9bN6y1I47Lsjes5Xkzm/KZ2HUj6vOCoGShzUiscvkZRGEAK7uATIRxawNjeYl+9wQBySA==
VmVrGQo2zRokW/ZuO9bN68viVmfoF0CTwLptyJBHtwoSZr9tnNwdtzzzsyNXbUiH4ZjqOIUbdd3rJE7TWaxfypVJqZu/Aj/SGb5km46+4nA=
hy0P42EdUgeeptYosmZKRIqHHtOztyzrdCi1Ki/o8rei5TptjILoB9DzYg4dhtD8
1u+XjG/2+GSQRv6EzCaWRQ==
3t+PWTTS6PQiwtwlmlTRxrj1Qed+wRQV5P7UkrnHcTJ09C2yMsencaTpdTOmxNS9
1u+XjG/2+GSQRv6EzCaWRQ==
W3A5Dt+8AxWSKsTYeXZoLzA4p6eOOqHFU6wo6i6M1wMfoECZ+LC1zy95fZWoQ/4A
VmVrGQo2zRokW/ZuO9bN6y/sHulJCdzqGiPnuEJh09GPOIF9J+niHBhGtw8fD45+pm0DBOHDbSGRphcx4joVf4s3jxWlrdo/+Xom1OuxaXU=
VmVrGQo2zRokW/ZuO9bN693VxLLTwZ5oiZ5WvX/IvvnPb+1eXq+YPA/fVCnI376l
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX0TLONUcGRzdgUZesuWNcR
VmVrGQo2zRokW/ZuO9bN69ju9su8sxzPM3AaGjlemMgcJAO2F+YRyjnWgbx5kZm3DDgmoVN2NnxtI1J/EBiaN3wzhoLg5uOydx3vmy3nBp0RYsKdXaZ0938zc7X1H9j+
1u+XjG/2+GSQRv6EzCaWRQ==
qEXluMqy4s3YaMlhK+yN2MTRTE+r7oIVVgr5FZa6wktdkLfItiTuEdAda4Prd/uu
NrBMtcyWGtR1hVD5pwJYfwziCIADCHuKVoMfBVeB7tfQcYQo9g3WMZE+QdDqr5d9
1u+XjG/2+GSQRv6EzCaWRQ==
wgR07xfoapmx6eEnFHXXYhJE4xrEpFjyOULkKEdGQ6xpKpQ97D+w2oBDgGLxKzYi
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX0TLONUcGRzdgUZesuWNcR
VmVrGQo2zRokW/ZuO9bN6ytmrKShFDNF/sBD9TyNb4M=
VmVrGQo2zRokW/ZuO9bN69ZEePYalO2NtKh63r2pCPdTwSWq+E1d5Vwp+OG0GW3QWB453G+Ivr5bi/FDqKsXgs+adjjgioHoNdPlikgjRC0=
1u+XjG/2+GSQRv6EzCaWRQ==
hCTaGH97j27PDhFRKBPIWx5h0aqe91PCFXPRyuarZwHCnfY5zg3rAGF8JC1Rlqi7
1u+XjG/2+GSQRv6EzCaWRQ==
OE58XJkoqje9oPDfqi73VlGkW2sntmceo8fDxcMP5lY1ekhmQtt77mccgEqJdX7o
bjpI32qzKze0nfrRYYQXmxQCptR8qXtM3eo8u7Hcpi52MCDBviv4HsElaaoUM2T8SccwLoZulXlCQSP1tUW0zQ==
f6i+lsi35nURDz6EJdx9zHkEwXP+oTAogJyWuMcbY+bCC4q6cHvMzOZJC/zSZqSlBoxWJ31FyRCKzYVEIwqIIGphFDgbiCs3jOWEnfTVYvpfqohCFOIPs7aAQ/e7qizKq2O1OuN6sW3wWSCv1R60FQ==
lNbaHNaSPRfUV3Bq+5wk64nrImxso5dDuILvGI+MQwTmWuTQaQzUpeGGa/V25O8w
1u+XjG/2+GSQRv6EzCaWRQ==
PWKGbNEPxxxpGpe8THO24yNGDxaAdaWP6Ho84adPD4xRdueQCK5aifGjt4a5kMSr
VmVrGQo2zRokW/ZuO9bN67Y8lER9Kabik7S1mV8J7BhJJn4LWRADsH3OSK3w2mDm
1u+XjG/2+GSQRv6EzCaWRQ==
1D18KWr2hdVsBcdZx1OaPRKa04PZd6Lh4pE5HE5I+6w=
VmVrGQo2zRokW/ZuO9bN67Y8lER9Kabik7S1mV8J7BjTWa3H547K7NAn0dgBK9APa9PKBQvJpaRJnxbK6GlinQ==
VmVrGQo2zRokW/ZuO9bN6/Q2k38eGwybF6UxQWGmZ078Ummo0l7USrgIMp2GbYxOX/mhLPY1elsTwCazaEKdmQ==
VmVrGQo2zRokW/ZuO9bN67Y8lER9Kabik7S1mV8J7Bj2jdAwm8k60quASDa57AGB+88A1mMb59gDR7BrxstZVQ==
VmVrGQo2zRokW/ZuO9bN6/Q2k38eGwybF6UxQWGmZ05NTezu3MlCHaWwWIcPk44PcMKo9jE/vk9iLOai9+rrxA==
VmVrGQo2zRokW/ZuO9bN67Y8lER9Kabik7S1mV8J7Bipb2FXNrO3nmjKp7FS8IV2D1PFSWGe34foTBexG5IW8NvIEINGi9MDK+xkxh/JWYQ=
VmVrGQo2zRokW/ZuO9bN67Y8lER9Kabik7S1mV8J7BiPv3Kfo9CJTrr9+UH2pEnYBMXobVjY6m+4Nub6BAneGA==
VmVrGQo2zRokW/ZuO9bN6/Q2k38eGwybF6UxQWGmZ07a+MiM4TasrtElO3rR88djBDttzYexwnhRBI9yaixsug==
VmVrGQo2zRokW/ZuO9bN67Y8lER9Kabik7S1mV8J7BiBPPnFp2y3W2sI/ics+tip6wIqnrMeHchezJBWwG4BnXlrvThfCLQ//csXUmX785Y=
VmVrGQo2zRokW/ZuO9bN65IkmxrkVECa18yoyhjHuJ62aVQkcicwTpC0ikCyAi/t
lNbaHNaSPRfUV3Bq+5wk6xOlK22tFRCz/MBWHqTO5bcv8iF8VlYARBwBFw2wdPus7zN51U2zCgV+IxIlEMlayQ==
l2FJPs4YkAmmok1ulDRuSA==
lNbaHNaSPRfUV3Bq+5wk67RG2evr7TLF3SKM8U8p2AA=
VmVrGQo2zRokW/ZuO9bN6+GpeDJLU0SmT7L+/5sE5xIlwrT8TwQpkYYGHjbVkNftN1CKdBCjUzympMQ9RT/FEg==
VmVrGQo2zRokW/ZuO9bN67oM2Yuk/fGj3koZ5Ii2OcjDkwr5hlr55yICOO4Y42+5
VmVrGQo2zRokW/ZuO9bN67jx0a4m693Et0R92z2wvL71x/3S2aSF/NvGCgu6CHqf
VmVrGQo2zRokW/ZuO9bN64Mo+xDfj7h1iI635dygp4XcwMixUpSGp93xEK87JQBN
VmVrGQo2zRokW/ZuO9bN6wbwiW2kEQvsTvAnLBH2llxizFasN29ZbVRWydu2b7CB
VmVrGQo2zRokW/ZuO9bN61gFm3fkb7F9QaTU95L/jPPifv6QNOCZefRXntGGdHLE
VmVrGQo2zRokW/ZuO9bN654Enj8yhWFV20SJ83piZWE=
YH231WTDnzQG3bFilBiqIg==
1u+XjG/2+GSQRv6EzCaWRQ==
1D18KWr2hdVsBcdZx1OaPRKa04PZd6Lh4pE5HE5I+6w=
VmVrGQo2zRokW/ZuO9bN67Y8lER9Kabik7S1mV8J7BgQnyGGSctkQxn38k19EVX1
VmVrGQo2zRokW/ZuO9bN6yJjT4k1EBcR3P33HPsJKcS2iJiueG/E56TTg8JuY5PMBrWa6U4B5U8rW1KDAZ2JSficZo3wFKS2XvW6ui8Tv2Y=
VmVrGQo2zRokW/ZuO9bN6/DCJtLswi79iRP2VS1Cl7m3kLpB5lzqL2Me52OhtZ1zUFOJHRYhWSvYLeuSZixvqmuGQwZgeXJcumVSIEgd/00=
VmVrGQo2zRokW/ZuO9bN6xRgv05zRD9dGM5M32jg6eA41WZXP7dtRrdMXjEoH3lvGk8OfKeqpDa0bqO9TtO5SbDD1FpNmhqJhoAYwQK56Qs=
VmVrGQo2zRokW/ZuO9bN619j4sjPeFT5nbRqDaMF1SytlAuchmjuOqjKzKLlRlpKp0wX5I7ca4hs64AaenatopykN6zoa1vKVmy5GNym1kk=
VmVrGQo2zRokW/ZuO9bN68Ys5qSHlhgxubR3riCCkMH07t3rA9i/RBuH8FqAjmcn82Cl0Yv2F59R3Y2ngEly5Bn5i06KWEOErKyGGIaEm5s=
VmVrGQo2zRokW/ZuO9bN654Enj8yhWFV20SJ83piZWE=
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN61MJdzJmpSSmQu1uh7zy38qILLEI2fsEffNYGvDUZieQ
1u+XjG/2+GSQRv6EzCaWRQ==
hCTaGH97j27PDhFRKBPIW4EZFPkfMH002JrNm9PLnXuwc9yq5Lyo6aOmZ30BZnoK
1u+XjG/2+GSQRv6EzCaWRQ==
2+RceSNjD2iq0sRzepMysYKB2p+0quZ3k/r81UP7jdA=
0tTy9hW41S4+Kusr1EouU1D1oliZ7e6CIIanBdYINco=
QSlGrRvW/rNBDeF7we6iBWKk2BA8QQKFrP2zSqI9P7kNT8zHpEzOqA9PqxnKhMkciutQQEuCUUTddAcDKQqmZtP+tM67XZ4sPVMO7+1M9AE=
B46DYlFQtDlulKRQCnJ3QWpbjBDDHLSb3vyz7F+mTm17hwykciIqoQznuIX47KmXxdbWZDnI+CY124mc9V20Ag==
IhuisKim47k91RVt8z8qtM11IfmUg3Vzb1vU4L+nUSEXI43SqtfmWpr/I8AYFS79jp/blpkCZsPn/8gvY1O0DDcOOIqpPruCuHCHNHbX95w=
96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz5S9puu5cR7ebyYVgOteskCUo9CHPdWLgUk6a7nUg/Dd/W6vX2Ar2YxjHE1Ly/3O18=
1u+XjG/2+GSQRv6EzCaWRQ==
Z02eW0OyMdUTGgS/rY5Ut6VY+CyNgF9FPBp9CnQzrMw=
Y8LE4fq4wJWePrU6pM0UJA/F7Cz7yGlo9qD3wRRTzkoqBjEYPeDTgDuU3vQKtZ/tslbdISWCHKSrP4sG9aOO6w==
c0jePRxtTVZYop75Q5JCFggVGiy5K37SY6O292Ia4MPkJnXEVXE/4VzqWDalKAbQ
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmWghjKa4Bx/9hwfBJOgnWLq5ZzPLUX/xqPUerYk2PoPsymFnJ5By9OX6QOc1ugur9I=
1u+XjG/2+GSQRv6EzCaWRQ==
3t+PWTTS6PQiwtwlmlTRxrj1Qed+wRQV5P7UkrnHcTJ09C2yMsencaTpdTOmxNS9
dh7w9F2NtRbFZXQAT43UNEb8EuN3AGhltXubCNFU3Ojzklfcg9iluJiEzQBZ1zSs
YnAdY30Lg/9fjxdn+Sc57EuR2oeC8XoCVhpUHNm7E15HYPr7Brw6YP/tAPp/5VcUf3nQ01NwI7KLps+jszazNg==
0bmFnPqs+8pv2SMKrUKBGjsjPAHPnQkQBpMrQ+IQDC0=
vxZRPtvgnx9GXrXB4G/UM7qbPAq4XIfzYp2SJP08XdQ=
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX0TLONUcGRzdgUZesuWNcR
VmVrGQo2zRokW/ZuO9bN69ju9su8sxzPM3AaGjlemMgcJAO2F+YRyjnWgbx5kZm3DDgmoVN2NnxtI1J/EBiaNy+VxOc/ziUJR5VmuD66oPA=
1u+XjG/2+GSQRv6EzCaWRQ==
qEXluMqy4s3YaMlhK+yN2MTRTE+r7oIVVgr5FZa6wktdkLfItiTuEdAda4Prd/uu
NrBMtcyWGtR1hVD5pwJYfwziCIADCHuKVoMfBVeB7tfQcYQo9g3WMZE+QdDqr5d9
1u+XjG/2+GSQRv6EzCaWRQ==
hCTaGH97j27PDhFRKBPIWx5h0aqe91PCFXPRyuarZwHCnfY5zg3rAGF8JC1Rlqi7
OE58XJkoqje9oPDfqi73VlGkW2sntmceo8fDxcMP5lZFXTmVGM0w7rfgyRt9m7p5
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX0TLONUcGRzdgUZesuWNcR
VmVrGQo2zRokW/ZuO9bN6ytmrKShFDNF/sBD9TyNb4M=
VmVrGQo2zRokW/ZuO9bN6xf5/2Z1ozJYK3IX9gEGYhudX+JDHzXKmtCPQeY89xZsmJ01Lxhee8vjMmUoN+52YwE0e8xmkBrpg0JAK3o4nqk=
1u+XjG/2+GSQRv6EzCaWRQ==
uNSTKhXsc886vN3euLSVYrxsmQUHvKax1CVDvgx4wzZ3gluW+Zte0pfXA1MbL3uR
1u+XjG/2+GSQRv6EzCaWRQ==
PAoN3u73Z3nXo35rPzNO3zk0x+iuneUZogH87K0TFCnqFSMDSQMOfHfmH+Dp486Z
0Ey56M3Rq4Z87mMZBzWCke79du117mx9ftztVnc2s4Y=
L0eUthVnpkGsmKFAX6d+uDz5ZRBys3d0SQKK5GYgSmX0TLONUcGRzdgUZesuWNcR
VmVrGQo2zRokW/ZuO9bN65W8leO0ggdGbf/NAsQSTaUN/Vkvk2lUEMRqevWdlxhOpMxrHYmMEeuI0pVjPC/pCg==
1u+XjG/2+GSQRv6EzCaWRQ==
yRP7zwdiezjpAYBvh+/YJI6Z+aRD7eDMoKmdruOi/nVKZCf2Kpe/EZS92lyLhXJw
S2kU4JuQ9t0SS+VL3mxwjQg60glazkUAeC35PDN/3gi5UC0kW/K3ybMcAsPxO6OqOno9DTOG+s+nbhv8quAd37RmBLZ4bWlOVxQB8830aY4=
VmVrGQo2zRokW/ZuO9bN65NDZuHjHfhtjN50qQqxWwhM0zCWzMVFfYkfFQXIapw7
cbvASHjpysrsjdY5RctXmCMIFkDgRpOy80SbmGIoVLlLES/Dr35tD/epk2Bblup2
L0eUthVnpkGsmKFAX6d+uBWSR38I0ooDiDs76zI3rHk=
sux3pEp0y4Ts+BK0jWjiPLanIepY87Lf1wBsVBlLv3Y=
32pdC9DD05OE2l0oXazDFCL2UDwltzVzeKrLP+917rjhOkK+v03ZP0I3EFsTN5wdo+TxLXpWwlAA41kIMUBwEdM0Vq0pElFIAqVtyfgDNJA=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HkmXtpUVC1kW3J/dl/fkcePcu0IQQNXefRtOGv4ZZf1LvA==
51UII3rZ87NZKsRBZ0eyLdVqoWpbfe8TmH2YJ9DWmQku+7dd9C856BngwQGeroz0DGruUOn3OzHXaFAtZ68Zrg==
2+RceSNjD2iq0sRzepMysYKB2p+0quZ3k/r81UP7jdA=
0tTy9hW41S4+Kusr1EouU1D1oliZ7e6CIIanBdYINco=
noar+Dkj/qVB9vIuoUrH4blDBjPJbLLUMktkKwDg+T3Yyejah7ZSTiS84g0cIqgmOkfT24OfZWuI2mlySo1e1rXnhvoN3FmpA8o1x0iDGmc=
VmVrGQo2zRokW/ZuO9bN69LIx3FNqnEXJvGI/6KUnVMBiTvkfBHbYk7rYKIjJcSR
IhuisKim47k91RVt8z8qtM11IfmUg3Vzb1vU4L+nUSEXI43SqtfmWpr/I8AYFS79jp/blpkCZsPn/8gvY1O0DDcOOIqpPruCuHCHNHbX95w=
96orka/uERLyRst14azQwnND1LaAMeJ6hPF2g6RNEz56MuFgEZqjEPy+8YvS/67ALmhmn5yIn6bMKi20dsYGHJ1JVuo73YMpR5XMGsGJGqk=
1u+XjG/2+GSQRv6EzCaWRQ==
uk/kSuwtaQ0nOezb8+RxpwUfyvjVM2PQ7j0JF7CVDyk=
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
aSrbsYE1ObLqGiiKHJGlHAcTCUngVNP7oPzhbRYTc1LguHKuTuQ19V/e58YOe1oL
vHflHGke4H/vMW5vfCuuUZkBDGb44m2BYpshIz0Qtv4=
OqT2rZxw+7PFqTI/7vaTYrDpIYlrorSNlnfPF+E80GZUtRwUSNGvcVbzeIQULPWq
0lIZluLVeEFaTSZzolqlcLJwgguKY+dpcMGzanWqRYgG35UDUa7t06U8mwJTvAIl
3HT+U1PE2cuTIdd28XDjLu4fELXwXCAvh+GRuQu5k6XIkFRLUhSeTkeWNqYg2bjX
661hZf7vhUQ+50okfwfTXw==
b4OJVZe8QyIpjuTpKXDL9A==
RVzqPMqMgbuirEDXLgMelnPNG1gGdxjJLuVE8z6iQD5rYH0fwwATzprmhCYaEIEPHwpSMPEmiRRpa14KTdZ8lrL8H7fceiNvq4Xjg2gDlfA=
mfT8DrNbUmxQ2BnZ1bZFTLh9MEuZKOpmAfF70OnZ9TU=
RVzqPMqMgbuirEDXLgMelvzcH7BOolbnVIZqDiEiOec=
1u+XjG/2+GSQRv6EzCaWRQ==
Nyrr9YR1SCpCbvnfcYsQdEJ5IErIE8c0cMFm3mG2Yjebpw+Cl1FLBTAZQhLw09+DifJASUH9cY0pNUe9YSHgQA==
1u+XjG/2+GSQRv6EzCaWRQ==
Nyrr9YR1SCpCbvnfcYsQdJPhbI7hgpVa6+b+pvEaZAgc7bOsYPSHQlBFlGcB4Tjs5+5PriIt/rre1MNSsEjYGg==
1u+XjG/2+GSQRv6EzCaWRQ==
OE58XJkoqje9oPDfqi73Vka6WpT+CnxlVbAMrhaI5qQllYRS+Ky/Og2S0buO4RDjLaK7aaOhQSDmoE/kbmyEX0/AwGUJ48Lz1OaTrJghsS4=
5p980mxWRpkxkeJsimzJ0eLt36Y+8CY+qHE8h6zZGmRXfw3tRaIncwdy2TcKbyGM
VmVrGQo2zRokW/ZuO9bN67r9iEkxlbKOXEHHo/GR3+6OlLSZTy+2hsIBqqaZN8zs2PwpJyZ9IxXb0V3MIkxqLA==
sobCGpmMf4/g7+HpPqBjC6hatZ6rUY3AAzqC73FJ58Q=
VmVrGQo2zRokW/ZuO9bN6zsF8RkKy/6qv8OwuTSXg5vBuddKZS4lQenCng06/MCtanygpIb/Fmy0N+f5854jLg==
lNbaHNaSPRfUV3Bq+5wk6xregBSeqbTc3HP8b1a2L+LfsHzyC7B1QGy1YoM9wHb8
lNbaHNaSPRfUV3Bq+5wk6x5c5FnmEkX4AMPnck73YtTfeAm/ehjF+VgLA6BASKYSOc0tdbZri4rI9oHgjk7DZ0y1lndySUX1LpsU+X4a+b4=
VmVrGQo2zRokW/ZuO9bN67l3wsMkIC1w41ITKeGhwtPvYwn0s/yxa7F2gxzMl8sIe2o/YPydyBKaXszPWarTog==
lNbaHNaSPRfUV3Bq+5wk6+MyMCLzOgYEev4vKFrT6VYswGLpldqwZFYz/mwTTxzi4N9Vog0YC88LxDAzU619Jg==
1u+XjG/2+GSQRv6EzCaWRQ==
lNbaHNaSPRfUV3Bq+5wk6wP9GMD3ujIvzKZlvdwah6xFKn9/N3/0c1ldYqNuhjD+DafNHKr4KQgIaI1li2boiw==
l2FJPs4YkAmmok1ulDRuSA==
5p980mxWRpkxkeJsimzJ0XxWkYe8AuDbH/wDJ9SMovWF4UqbpzK2e1xLE+WLbzyS
VmVrGQo2zRokW/ZuO9bN64uAF5aDWHLl0Mr6n5QG1UKr9GBXEgStOxmayztSy2KXpV2gQCOW03tZ9GFDtTtwKg==
1u+XjG/2+GSQRv6EzCaWRQ==
Qqw7/vtLTsLpPtVEcrxvqt1KzLgzU6i+9fBX4qg6S06lwIrFxe7lO1lW93JTA8QUi+I+L9IoSN9HvHBBfimTuQ==
EJoGFvRvZ0ApgLDRyrlLhA==
IhuisKim47k91RVt8z8qtLgFgQIxiwgnyWmJTJdd9h3En7oUD2HpPW3NDaRGcUSt6YoEknERcJPsfCBaPB4nCCEZAIRq1lDGynvHBaj+qnU=
1u+XjG/2+GSQRv6EzCaWRQ==
ZmnvqmOaBVPZA11BQvdr3w==
+qdJgONcot1WKgm0a/QSWRfjD/63UhGjDDaD83ombAk=
5sfgsihl9XVF/Mhh9qm1EXf+NRhU/cymXz1HbW9F6P0AEvJeZudYCA+pkRXe4Uye
hCTaGH97j27PDhFRKBPIW/dJaxrfoD6RixVo3Rf5Yz3CGP+VwV5No6W32fmew1fv
1u+XjG/2+GSQRv6EzCaWRQ==
uNSTKhXsc886vN3euLSVYkem6h/gL5IHP/XBe2yL1bo=
RDZxR0eU/0YxwhYtovyGeQdnHApsoYjk/m7WJoznATFPayg1JrxurOnrWeCKYGPxeVB/Pw06PW3t/mffVKTZXA==
5p980mxWRpkxkeJsimzJ0cW/XPkGPu2EZf497lktZsIIxlZhnrw6R5u2fNEYrFnx
VmVrGQo2zRokW/ZuO9bN60btJ3KSmT5EZdF/tXV227NT+FaJI6ICAUv8U1dGM7fI
VmVrGQo2zRokW/ZuO9bN665BzXWwO22NoRyeko8p9TvBNbaZ4mmPHkdSjtdhxjFs
VmVrGQo2zRokW/ZuO9bN68iybrt8ScY6aQlDmfS7eSM=
1u+XjG/2+GSQRv6EzCaWRQ==
OE58XJkoqje9oPDfqi73Vls284DI6YzG6XdVqkLU4Lg=
L0eUthVnpkGsmKFAX6d+uMq25m1vOsGY6uP5qR9ZDEk=
1u+XjG/2+GSQRv6EzCaWRQ==
Dx/Tamsgq45f2G8qqPP8mNKTgXHLg2Jqs99bb34tU2xwBPXgAea9fPTD7mOcju2nndDdcCRYG/Dk9k8gfM4mlQ==
1u+XjG/2+GSQRv6EzCaWRQ==
OE58XJkoqje9oPDfqi73Vka6WpT+CnxlVbAMrhaI5qTvWyB5POI819VyoubLgWwB5otU5A2wKU0FdlIfCfOYuwe8tZUX3UkX2C3v16xcahM=
L0eUthVnpkGsmKFAX6d+uEiHn3ZMQfz1ZBQJd6m1KmU=
aPdLEV6HY2SBr9CPW/3NlV6skNvI9JsIj9pOVhf2RvA=
1u+XjG/2+GSQRv6EzCaWRQ==
FNzrf0lVMea1y0IWwp4L8epdqcTQLISFJkRRoX88WUg=
YsP/lfsk7bnmh2Eb9pKHkx7tWRnRvXwXah4j7+bCanMiqa4Sv8rxo4QlIJtxoVVB+SYW2kzxPd3PjQ9b/8SThg==
Z8UsPk1Q7HtwjRd4g01ryw==
mGGXRHZ5m7FaXP7i8r+TjKXApLbXwo2hRd9B7Q+Oh+YScyp7CR8x/JMkWUjHmH2e
Z8UsPk1Q7HtwjRd4g01ryw==
Djb8zu0gG2jt979SRwXXjOvVYuDaNNQCNmI1OvvoD70=
1u+XjG/2+GSQRv6EzCaWRQ==
qEXluMqy4s3YaMlhK+yN2MTRTE+r7oIVVgr5FZa6wktdkLfItiTuEdAda4Prd/uu
1u+XjG/2+GSQRv6EzCaWRQ==
hCTaGH97j27PDhFRKBPIWx5h0aqe91PCFXPRyuarZwHCnfY5zg3rAGF8JC1Rlqi7
PAoN3u73Z3nXo35rPzNO3zk0x+iuneUZogH87K0TFCnqFSMDSQMOfHfmH+Dp486Z
0Ey56M3Rq4Z87mMZBzWCkcIFCFur1J8qdEROFpa5DTfPmhRCLNzFjK3BYhcI7wHL
b4OJVZe8QyIpjuTpKXDL9A==
wlLHv6kT3Q/RmtMBN4nDAUsRm/HZvlCR+HWwmmPd5BfKH0/mlXTbyd8F5IPAE/TQu6d4aGXkkjKrV3KQBzmzVA==
VmVrGQo2zRokW/ZuO9bN68lFj6lfd3kckH/8VU5KCGqRWPiKTSyoPv5Bah2WfmYPVCp2VdHIolYjln+bUY8IfA==
VmVrGQo2zRokW/ZuO9bN6wN0w1RlCvEbh31Ln2UXkZ4=
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN64qGHGLHzR3bJ0XTpYCKgYgQpBdbM26ZAvopx/0wQyEoMC3CmrgnHMvfx+hntjfNUHY7L2qLZPXkr8UaUm3Cy0k=
VmVrGQo2zRokW/ZuO9bN6xlpjbR96JcinCRcpOHpwZenlGkZdJSeRDRdYj+Uxd4Q
VmVrGQo2zRokW/ZuO9bN63FDXhc/zgNdUygJevY6sdfklx6SK4YjsNl6mTDBAKgE8h7HBiUUTA9Bt0KMzmPRKQ==
VmVrGQo2zRokW/ZuO9bN674gG529I8kaxTDpeI5FsT8dMvpo4da/IxkpsnqO1PV6
VmVrGQo2zRokW/ZuO9bN67ldobmDDoizoaOC01WU6Lefy2U3Vi+qnHg+Yz87bA7Yx0kQAUrar1aLZvBPsQzYNQ==
VmVrGQo2zRokW/ZuO9bN641bT1nOxoWTwSy5bjmJpALq5/7D5eyKkgIFXHllLBCM
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN6zr+/1wvjvcA1PfTZ75ETzhfDdosE8sRI6toDfDVa/B+GA3ZxX/sQ5x8rNHncB67PQ==
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN67bnnwCzLN0nCiOfYCjwGkzY3B74QLecUPd3/scZhk0IehNEkYTW5l8MuaPFKHqVdQ==
VmVrGQo2zRokW/ZuO9bN68lvNCUThol9QOZsTvs3KfyGyJbrExzCQ6mfnSmO05oAUT0VDwsgBOA+xBAbY1o7AH+CI7nyhOXcP2xDCSHKZcc=
VmVrGQo2zRokW/ZuO9bN6y1m/1hYuCtHRuLugwo8qhGp3RtxJPr/0R9/8numyyW4RR2GSU+QZ1wJB5832QK4WA==
VmVrGQo2zRokW/ZuO9bN6zUY/ks5sODBNkit+XUmGYfULT1Hm3kjx3OllDbqxCp5
VmVrGQo2zRokW/ZuO9bN60uVXYgkwzKRDFzxQBIq76qRjSWU3fpXZBItYruE986KEffW/VhhUdVZm89YsKusT5fg0PTP+/voESni1r2r/mo=
VmVrGQo2zRokW/ZuO9bN6zTqNbvPfLl0muLinRFsIVJnX8w6F7OF6czp+acnqKok
VmVrGQo2zRokW/ZuO9bN664k4tNeuaruJ/sFk8Bpdhd+Kuw7EwE3k6tuVPiwyQPh/dbbmXLoyDryQD+C7WG6IA==
VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw=
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN68lFj6lfd3kckH/8VU5KCGpL8uDM5jESUEVKzanX7SIT34Qe4HuSzT9CzY9onFg14DakCsvU0GyUHbn02itcG7k=
VmVrGQo2zRokW/ZuO9bN6yzC2K2s9gZH1JCopKV0Ar4x5AppikyVQmfuHMxIKL83UsUGiLw1dPHTpVO8OXszJyMOhx9I9L4ie7f5yGBz7F4=
VmVrGQo2zRokW/ZuO9bN6yDqekvzHp5Ed5o8lYwu08gQvbBnlCFR6OzQBk0zuhcJ
VmVrGQo2zRokW/ZuO9bN6+mC4aS+uqusFmsL8BytKn/hS7c7ij9TJiyQy60d4c0NqLXgocb8PZ3sNsFMa3Nip++r3rnQ90me5kM1X+GMQ30=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpglmoyr/KfGS/F8kfb7XBOQmFuNPgF6sTpiQlwU0wFj4=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpyz1Asb8tJdmNZVt1y9C5pjJdvLrwYyZILmakF/bC+zM=
VmVrGQo2zRokW/ZuO9bN693SjNGuvN8cXdnK0A7Y+ycnGy0ZQfSO3nE81pPoyyp3U4SKGgGb3zIB5poznxhWT0VsGXdaYJ0qyM+igR1cz+8=
VmVrGQo2zRokW/ZuO9bN6+6OSQjJyDHPNRB3fSq3EOcAoWNzFMgvgUAWiGAmZDd3
VmVrGQo2zRokW/ZuO9bN62BvU+ACUylwdsM3AEh5vxG9ahubQdJ/24TjNy1jqCqN
VmVrGQo2zRokW/ZuO9bN6zxA5tAr3l0brvCSLFOCVTe2rbsn8iecC4QUX6qqquupab5ALX5SWCmXowW86sbKKoKxmn3G+92voYN1oleYcRHhyMTKXW/xSk9Vbu3H4Ryh
VmVrGQo2zRokW/ZuO9bN67FRjyUL790gahogpF4bk/0KuYf1lD1XRfqDBFgwVpiQu8K990tiW+jZiLy8UgeJYg==
VmVrGQo2zRokW/ZuO9bN620zSkiHBXev6YTvy3qjpOw=
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN68lFj6lfd3kckH/8VU5KCGpL8uDM5jESUEVKzanX7SITo87IQQenhDLL5E41UYEEt1zKN51FxDrrT3NlL/bThGh9fv4aJdoH/yPu/bkEggBJ
VmVrGQo2zRokW/ZuO9bN66pSy2yezMgXP8gSS2GK+PpDegwDKFSH1dYfVl26gnZj1jhP7kPEVPumIlOzBJ+DIJ2PF+2le2jE5SDVCLl1NiE=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehp5dTPBR/trYVhdp27jkIn6J6H5oHikRa60ngWLPNBVzo=
VmVrGQo2zRokW/ZuO9bN61IUDnAmfgBQO6uHH1Yclt0ynNsjH12Q/lM4PiHYK8Yh
VmVrGQo2zRokW/ZuO9bN60JKAP0f5b6Sb7s9Js4pTMpTnlWWsEzXQ+p858xVCqurEIhq7VC4BSJPHvWQ57VJoQ==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hknz/J5UNnSr5HoDQfOjMQiy6/uL7ISdH22fNdwzZ/8iWxubQxhh6seRqDUg0oS1XIAit7c7VNhMHdAx5zY4Vqy+
VmVrGQo2zRokW/ZuO9bN6ylgXkDyP4zJff9d51ElvAIMq3E5W8e//ErL9duy8ksPrIEGzcQJJw7VFLvdP0ogWi8sDVJaJcJxoKhXOn6xIBk=
VmVrGQo2zRokW/ZuO9bN6/Ykiia9pTHWG10nmQ2azo1nONlRLILx+L82bJzKIMHd
VmVrGQo2zRokW/ZuO9bN6zn88KEm9OgxN0QITEeDgHR9RpFiPd0ZwiLF5lEABccP
VmVrGQo2zRokW/ZuO9bN6/isbQJniBo16ObIGPZRjcFiPqV4uBr2OpohSuDyZjLt0XGVHG4wpO4VVc3reFGBB+/6OMzyTMioQa0/nD9ejqTVB3eW8hcBPdPA6Kl5IG1cw/LW+q0gxEzhV+EvhQTsYA==
VmVrGQo2zRokW/ZuO9bN62dUIyY4EVuFnYchLLg1Q9bs+s6edwxQpeiikyW8nG2yNYj6TTFZTrmMt1EQS41HJA==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknNX4uUDjKKQHFJozt8cgdhI80NVDlJ98fuWbEMsUykomMbyoRzTFmFF5uSKeJoOAw=
1u+XjG/2+GSQRv6EzCaWRQ==
VmVrGQo2zRokW/ZuO9bN67focE/OItqiH8RxmfzhIw3c9UUf6KvpDEhdOqh/QOWs/1EqaCMGyBSwFbIse4JBKjHHHkNKma+31mNrfboMY3c=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpFeyaEx6TSSxDiBlioSN7WSfpsecKcmnjxXZb+dyTMEM=
VmVrGQo2zRokW/ZuO9bN6yDqekvzHp5Ed5o8lYwu08gQvbBnlCFR6OzQBk0zuhcJ
VmVrGQo2zRokW/ZuO9bN6+mC4aS+uqusFmsL8BytKn/hS7c7ij9TJiyQy60d4c0NqLXgocb8PZ3sNsFMa3Nip++r3rnQ90me5kM1X+GMQ30=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpglmoyr/KfGS/F8kfb7XBOQmFuNPgF6sTpiQlwU0wFj4=
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6Hkl8sUPd5ZHgQChuaaRMaehpyz1Asb8tJdmNZVt1y9C5pjJdvLrwYyZILmakF/bC+zM=
VmVrGQo2zRokW/ZuO9bN693SjNGuvN8cXdnK0A7Y+ycnGy0ZQfSO3nE81pPoyyp3U4SKGgGb3zIB5poznxhWT0VsGXdaYJ0qyM+igR1cz+8=
VmVrGQo2zRokW/ZuO9bN6+6OSQjJyDHPNRB3fSq3EOcAoWNzFMgvgUAWiGAmZDd3
VmVrGQo2zRokW/ZuO9bN62BvU+ACUylwdsM3AEh5vxG9ahubQdJ/24TjNy1jqCqN
VmVrGQo2zRokW/ZuO9bN6zxA5tAr3l0brvCSLFOCVTeGK1EDQpaQ4W+HZzdN7+BB2ZUl65M4XyjU/xYSEFyczi5NDLlf8Rw3rPFXMprgLYPL6UBJd3EBOVlSZi16q0v8cVHtqJGKa0gzXjpjel/S2Q==
VmVrGQo2zRokW/ZuO9bN67FRjyUL790gahogpF4bk/0KuYf1lD1XRfqDBFgwVpiQZsZqUVnw544DWr0gXit21Q==
VmVrGQo2zRokW/ZuO9bN64ZTCtjiSIe1Vj/8GYr6HknzRBBxXsj0OoebX5mHuVG/bP0kyt0UFdIMy35sdT+Xlx9CbxX3haSizx9Fd1+tPwU=
A50UO/kAI17YP7MCbTvBkFTjN5Ay5WDQtfGIvrhR2tvkyujMWxwi3omnSXsVaboD
s6ChnXH5zaR4nss2Jj7ULBibRmB/kmin0eYU9S2eTfU=
IhuisKim47k91RVt8z8qtM11IfmUg3Vzb1vU4L+nUSEXI43SqtfmWpr/I8AYFS79jp/blpkCZsPn/8gvY1O0DDcOOIqpPruCuHCHNHbX95w=
lIrvZaQ6CfSmdKebjwiKcKdbPCIaBJUdHz+nu4YDNU8=
1u+XjG/2+GSQRv6EzCaWRQ==
a7rKHSoe96bTC1qOWHQhNXgPTFVeAmUicD8E9nHKDLE=
6we+bQ+BNhpl74ICwbzDDxCOR7eR1lYIi6R1B7qE6VQq5fLF725RdYF6Y+savDkL
dHP8w7bwYQXmb0DilW0aGoiXjYmTX2tV1zDoCXSG5x0=
oPiee0X1lhqQ76kw9N2+8TRpQ8dHbXh5l9s3hEzztTi7WxTx3RYObFQX4hoEVzqA
oPiee0X1lhqQ76kw9N2+8VJVl0MJWad+iXT4tHQ1qJM=
S8MkKIBd1s9qx4qteuqTKYtNNok7S0W1D9yy/JgdcKA=
UqaKSIk9RYYWXSK0cIFfXhsSTGfp/1ffoQCd4abr94o=
39tsr9ptyNKBswzHAsYTwQNhpjS5ZgfHk3NXI3mGlC8=
hC9Wqf+oYT8RvKOl1V/o2sfBwmNUWuaRvOIXJpHTuAM=
661hZf7vhUQ+50okfwfTXw==
oPiee0X1lhqQ76kw9N2+8SoJ8C9QFPZokIoCmoh0qvPxsretPquTnGN79JC/ZUu8
CAhn8dRUItEbEErp4w+lXwA7HvkR+MU9AKdUt6vt65vn7GpKaNuQvNmOChuosmFK

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,377 @@
# coding: utf-8
# +-------------------------------------------------------------------
# | YakPanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
import sqlite3
import os, time, sys
os.chdir('/www/server/panel')
if not 'class/' in sys.path:
sys.path.insert(0, 'class/')
import public
class Sql():
# ------------------------------
# 数据库操作类 For sqlite3
# ------------------------------
__DB_FILE = None # 数据库文件
__DB_CONN = None # 数据库连接对象
__DB_TABLE = "" # 被操作的表名称
__OPT_WHERE = "" # where条件
__OPT_LIMIT = "" # limit条件
__OPT_ORDER = "" # order条件
__OPT_FIELD = "*" # field条件
__OPT_PARAM = () # where值
def __init__(self, dbfile=None):
if not os.path.exists("class/projectModel/content/"):
os.makedirs("class/projectModel/content/")
if not dbfile:
self.__DB_FILE = 'class/projectModel/content/content.db'
self.__LOCK = '/dev/shm/{}.pl'.format(self.__DB_FILE.replace('/', '_'))
else:
self.__DB_FILE ='class/projectModel/content/' + dbfile + '.db'
self.__LOCK = '/dev/shm/{}.pl'.format(self.__DB_FILE.replace('/', '_'))
if not os.path.exists(self.__DB_FILE):
# 创建数据库
conn = sqlite3.connect(self.__DB_FILE)
conn.close()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_trackback):
self.close()
def __GetConn(self):
# 取数据库对象
try:
if self.__DB_CONN == None:
self.__DB_CONN = sqlite3.connect(self.__DB_FILE)
self.__DB_CONN.text_factory = str
except Exception as ex:
return "error: " + str(ex)
def dbfile(self, name):
self.__DB_FILE = 'class/projectModel/content/' + name + '.db'
return self
def table(self, table):
# 设置表名
self.__DB_TABLE = table
return self
def where(self, where, param):
# WHERE条件
if where:
self.__OPT_WHERE = " WHERE " + where
self.__OPT_PARAM = self.__to_tuple(param)
return self
def __to_tuple(self, param):
# 将参数转换为tuple
if type(param) != tuple:
if type(param) == list:
param = tuple(param)
else:
param = (param,)
return param
def order(self, order):
# ORDER条件
if len(order):
self.__OPT_ORDER = " ORDER BY " + order
return self
def limit(self, limit):
# LIMIT条件
if len(limit):
self.__OPT_LIMIT = " LIMIT " + limit
return self
def field(self, field):
# FIELD条件
if len(field):
self.__OPT_FIELD = field
return self
def select(self):
# 查询数据集
self.__GetConn()
try:
self.__get_columns()
sql = "SELECT " + self.__OPT_FIELD + " FROM " + self.__DB_TABLE + self.__OPT_WHERE + self.__OPT_ORDER + self.__OPT_LIMIT
result = self.__DB_CONN.execute(sql, self.__OPT_PARAM)
data = result.fetchall()
# 构造字典系列
if self.__OPT_FIELD != "*":
fields = self.__format_field(self.__OPT_FIELD.split(','))
tmp = []
for row in data:
i = 0
tmp1 = {}
for key in fields:
tmp1[key.strip('`')] = row[i]
i += 1
tmp.append(tmp1)
del (tmp1)
data = tmp
del (tmp)
else:
# 将元组转换成列表
tmp = list(map(list, data))
data = tmp
del (tmp)
self.__close()
return data
except Exception as ex:
return "error: " + str(ex)
def get(self):
self.__get_columns()
return self.select()
def __format_field(self, field):
import re
fields = []
for key in field:
s_as = re.search(r'\s+as\s+', key, flags=re.IGNORECASE)
if s_as:
as_tip = s_as.group()
key = key.split(as_tip)[1]
fields.append(key)
return fields
def __get_columns(self):
if self.__OPT_FIELD == '*':
tmp_cols = self.query('PRAGMA table_info(' + self.__DB_TABLE + ')', ())
cols = []
for col in tmp_cols:
if len(col) > 2: cols.append('`' + col[1] + '`')
if len(cols) > 0: self.__OPT_FIELD = ','.join(cols)
def getField(self, keyName):
# 取回指定字段
try:
result = self.field(keyName).select()
if len(result) != 0:
return result[0][keyName]
return result
except:
return None
def setField(self, keyName, keyValue):
# 更新指定字段
return self.save(keyName, (keyValue,))
def find(self):
# 取一行数据
try:
result = self.limit("1").select()
if len(result) == 1:
return result[0]
return result
except:
return None
def count(self):
# 取行数
key = "COUNT(*)"
data = self.field(key).select()
try:
return int(data[0][key])
except:
return 0
def add(self, keys, param):
# 插入数据
self.write_lock()
self.__GetConn()
self.__DB_CONN.text_factory = str
try:
values = ""
for key in keys.split(','):
values += "?,"
values = values[0:len(values) - 1]
sql = "INSERT INTO " + self.__DB_TABLE + "(" + keys + ") " + "VALUES(" + values + ")"
result = self.__DB_CONN.execute(sql, self.__to_tuple(param))
id = result.lastrowid
self.__close()
self.__DB_CONN.commit()
self.rm_lock()
return id
except Exception as ex:
return "error: " + str(ex)
# 插入数据
def insert(self, pdata):
if not pdata: return False
keys, param = self.__format_pdata(pdata)
return self.add(keys, param)
# 更新数据
def update(self, pdata):
if not pdata: return False
keys, param = self.__format_pdata(pdata)
return self.save(keys, param)
# 构造数据
def __format_pdata(self, pdata):
keys = pdata.keys()
keys_str = ','.join(keys)
param = []
for k in keys: param.append(pdata[k])
return keys_str, tuple(param)
def addAll(self, keys, param):
# 插入数据
self.write_lock()
self.__GetConn()
self.__DB_CONN.text_factory = str
try:
values = ""
for key in keys.split(','):
values += "?,"
values = values[0:len(values) - 1]
sql = "INSERT INTO " + self.__DB_TABLE + "(" + keys + ") " + "VALUES(" + values + ")"
result = self.__DB_CONN.execute(sql, self.__to_tuple(param))
self.rm_lock()
return True
except Exception as ex:
return "error: " + str(ex)
def commit(self):
self.__close()
self.__DB_CONN.commit()
def save(self, keys, param):
# 更新数据
self.write_lock()
self.__GetConn()
self.__DB_CONN.text_factory = str
try:
opt = ""
for key in keys.split(','):
opt += key + "=?,"
opt = opt[0:len(opt) - 1]
sql = "UPDATE " + self.__DB_TABLE + " SET " + opt + self.__OPT_WHERE
# 处理拼接WHERE与UPDATE参数
tmp = list(self.__to_tuple(param))
for arg in self.__OPT_PARAM:
tmp.append(arg)
self.__OPT_PARAM = tuple(tmp)
result = self.__DB_CONN.execute(sql, self.__OPT_PARAM)
self.__close()
self.__DB_CONN.commit()
self.rm_lock()
return result.rowcount
except Exception as ex:
return "error: " + str(ex)
def delete(self, id=None):
# 删除数据
self.write_lock()
self.__GetConn()
try:
if id:
self.__OPT_WHERE = " WHERE id=?"
self.__OPT_PARAM = (id,)
sql = "DELETE FROM " + self.__DB_TABLE + self.__OPT_WHERE
result = self.__DB_CONN.execute(sql, self.__OPT_PARAM)
self.__close()
self.__DB_CONN.commit()
self.rm_lock()
return result.rowcount
except Exception as ex:
return "error: " + str(ex)
def execute(self, sql, param=()):
# 执行SQL语句返回受影响行
self.write_lock()
self.__GetConn()
try:
result = self.__DB_CONN.execute(sql, self.__to_tuple(param))
self.__DB_CONN.commit()
self.rm_lock()
return result.rowcount
except Exception as ex:
return "error: " + str(ex)
# 是否有锁
def is_lock(self):
n = 0
while os.path.exists(self.__LOCK):
n += 1
if n > 100:
self.rm_lock()
break
time.sleep(0.01)
# 写锁
def write_lock(self):
self.is_lock()
with open(self.__LOCK, 'wb+') as f:
f.close()
# 解锁
def rm_lock(self):
if os.path.exists(self.__LOCK):
try:
os.remove(self.__LOCK)
except:
pass
def query(self, sql, param=()):
# 执行SQL语句返回数据集
self.__GetConn()
try:
result = self.__DB_CONN.execute(sql, self.__to_tuple(param))
# 将元组转换成列表
data = list(map(list, result))
return data
except Exception as ex:
return "error: " + str(ex)
def create(self, name):
# 创建数据表
self.write_lock()
self.__GetConn()
script = public.readFile('data/' + name + '.sql')
result = self.__DB_CONN.executescript(script)
self.__DB_CONN.commit()
self.rm_lock()
return result.rowcount
def fofile(self, filename):
# 执行脚本
self.write_lock()
self.__GetConn()
script = public.readFile(filename)
result = self.__DB_CONN.executescript(script)
self.__DB_CONN.commit()
self.rm_lock()
return result.rowcount
def __close(self):
# 清理条件属性
self.__OPT_WHERE = ""
self.__OPT_FIELD = "*"
self.__OPT_ORDER = ""
self.__OPT_LIMIT = ""
self.__OPT_PARAM = ()
def close(self):
# 释放资源
try:
self.__DB_CONN.close()
self.__DB_CONN = None
except:
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,278 @@
# coding: utf-8
# +-------------------------------------------------------------------
# | YakPanel
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved.
# +-------------------------------------------------------------------
# | Wordpress 安全扫描
# +--------------------------------------------------------------------
import re
import os
#进入到
from projectModelV2 import totle_db
class wordpress_scan:
#默认插件的头部信息
plugin_default_headers = {
"Name": "Plugin Name",
"PluginURI": "Plugin URI",
"Version": "Version",
"Description": "Description",
"Author": "Author",
"AuthorURI": "Author URI",
"TextDomain": "Text Domain",
"DomainPath": "Domain Path",
"Network": "Network",
"RequiresWP": "Requires at least",
"RequiresPHP": "Requires PHP",
"UpdateURI": "Update URI",
"RequiresPlugins": "Requires Plugins",
"_sitewide": "Site Wide Only"
}
#默认主题的头部信息
theme_default_headers = {
"Name": "Theme Name",
"Title": "Theme Name",
"Version": "Version",
"Author": "Author",
"AuthorURI": "Author URI",
"UpdateURI": "Update URI",
"Template": "Theme Name",
"Stylesheet": "Theme Name",
}
def M(self, table):
'''
@name 获取数据库对象
@param table 表名
@param db 数据库名
'''
with totle_db.Sql().dbfile("../wordpress") as sql:
return sql.table(table)
def get_plugin_data(self, plugin_file, default_headers, context=''):
'''
@参考:/wp-admin/includes/plugin.php get_plugin_data 代码
@name 获取插件信息
@param plugin_file 插件文件
@return dict
@auther lkq
@time 2024-10-08
'''
# 读取文件内容
if not os.path.exists(plugin_file): return {}
# 定义8KB大小
max_length = 8 * 1024 # 8 KB
try:
# 读取文件的前8KB
with open(plugin_file, 'r', encoding='utf-8') as file:
file_data = file.read(max_length)
except Exception as e:
return {}
# 替换CR为LF
file_data = file_data.replace('\r', '\n')
# 处理额外的headers
extra_headers = {}
if context:
extra_context_headers = []
# 假设有一个函数可以获取额外的headers
# extra_context_headers = get_extra_headers(context)
extra_headers = dict.fromkeys(extra_context_headers, '') # 假设额外的headers
all_headers = {**extra_headers, **default_headers}
# 检索所有headers
for field, regex in all_headers.items():
if field.startswith('_'): # 跳过以_开头的内部字段
continue
match = re.search(f'{regex}:(.*)$', file_data, re.IGNORECASE | re.MULTILINE)
if match:
all_headers[field] = match.group(1).strip()
else:
all_headers[field] = ''
if all_headers.get("Network") and not all_headers['Network'] and all_headers['_sitewide']:
all_headers['Network'] = all_headers['_sitewide']
if all_headers.get("Network"):
all_headers['Network'] = 'true' == all_headers['Network'].lower()
if all_headers.get("_sitewide"):
del all_headers['_sitewide']
if all_headers.get("TextDomain") and not all_headers['TextDomain']:
plugin_slug = os.path.dirname(os.path.basename(plugin_file))
if '.' != plugin_slug and '/' not in plugin_slug:
all_headers['TextDomain'] = plugin_slug
all_headers['Title'] = all_headers['Name']
all_headers['AuthorName'] = all_headers['Author']
# 返回插件的信息
return all_headers
def Md5(self,strings):
"""
@name 生成MD5
@author hwliang <hwliang@yakpanel.com>
@param strings 要被处理的字符串
@return string(32)
"""
if type(strings) != bytes:
strings = strings.encode()
import hashlib
m = hashlib.md5()
m.update(strings)
return m.hexdigest()
def FileMd5(self,filename):
"""
@name 生成文件的MD5
@author hwliang <hwliang@yakpanel.com>
@param filename 文件名
@return string(32) or False
"""
if not os.path.isfile(filename): return False
import hashlib
my_hash = hashlib.md5()
f = open(filename, 'rb')
while True:
b = f.read(8096)
if not b:
break
my_hash.update(b)
f.close()
return my_hash.hexdigest()
def get_plugin(self, path,one=''):
'''
@name 获取WordPress插件信息
@param path 插件路径
@return dict
@auther lkq
@time 2024-10-08
'''
plugin_path = path + "/wp-content/plugins"
if not os.path.exists(plugin_path): return {}
tmp_list = []
for file in os.listdir(plugin_path):
if one:
if file!=one:continue
plugin_file = os.path.join(plugin_path, file)
# if os.path.isfile(plugin_file) and plugin_file.endswith(".php"):
# tmp_list.append(file)
if os.path.isdir(plugin_file):
# 读取文件夹中的第一层文件
for file2 in os.listdir(plugin_file):
plugin_file2 = os.path.join(plugin_file, file2)
if os.path.isfile(plugin_file2) and plugin_file2.endswith(".php"): tmp_list.append(
file + "/" + file2)
if len(tmp_list) == 0: return {}
result = {}
for i in tmp_list:
plugin_file = plugin_path + "/" + i
# 判断文件是否可读
if not os.access(plugin_file, os.R_OK): continue
plugin_data = self.get_plugin_data(plugin_file, self.plugin_default_headers)
if not plugin_data: continue
if plugin_data["Name"] == "": continue
#如果 name 中没/ 的话
if "/" not in i:
#则判断一下
if 'wordpress.org/plugins/' in plugin_data["PluginURI"]:
plugin_data["PluginURI"] = plugin_data["PluginURI"].replace('http://wordpress.org/plugins/', '').replace("http://wordpress.org/plugins/","")
#去掉最后的/
if plugin_data["PluginURI"][-1]=="/":
plugin_data["PluginURI"]=plugin_data["PluginURI"][:-1]
i=plugin_data["PluginURI"]
else:
continue
result[i] = plugin_data
return result
def compare_versions(self,version1, version2):
'''
@name 对比版本号
@param version1 版本1
@param version2 版本2
@return int 0 相等 1 大于 -1 小于
'''
# 分割版本号为整数列表
v1 = [int(num) for num in version1.split('.')]
v2 = [int(num) for num in version2.split('.')]
# 逐个比较版本号的每个部分
for num1, num2 in zip(v1, v2):
if num1 > num2:
return 1 # version1 > version2
elif num1 < num2:
return -1 # version1 < version2
# 如果所有部分都相同,比较长度(处理像'1.0'和'1.0.0'这样的情况)
if len(v1) > len(v2):
return 1 if any(num > 0 for num in v1[len(v2):]) else 0
elif len(v1) < len(v2):
return -1 if any(num > 0 for num in v2[len(v1):]) else 0
# 如果完全相同
return 0
def let_identify(self,version,vlun_infos):
'''
@name 对比版本号判断是否存在漏洞
@param version 当前版本
@param vlun_infos 漏洞信息
@return list
'''
for i in vlun_infos:
i["vlun_status"] = False
#如果是小于等于的话
if i["let"]=="<=":
if self.compare_versions(version,i["vlun_version"])<=0:
i["vlun_status"]=True
#小于
if i["let"]=="<":
if self.compare_versions(version,i["vlun_version"])<0:
i["vlun_status"]=True
if i['let']=='-':
#从某个版本开始、到某个版本结束
version_list=i["vlun_version"].split("-")
if len(version_list)!=2:continue
if self.compare_versions(version,version_list[0])>=0 and self.compare_versions(version,version_list[1])<=0:
i["vlun_status"]=True
return vlun_infos
def scan(self,path):
'''
@name 扫描WordPress
@param path WordPress路径
@return dict
@auther lkq
@time 2024-10-10
@msg 通过扫描WordPress的版本、插件、主题来判断是否存在漏洞
'''
vlun_list = []
#判断文件是否存在
import os
if not os.path.exists(path):
return vlun_list
result = {}
result["plugins"] = self.get_plugin(path)
#扫描插件是否存在漏洞
for i in result["plugins"]:
plguin=i.split("/")[0]
Name=result["plugins"][i]["Name"]
if result["plugins"][i]["Version"]=="":continue
#检查插件是否存在漏洞
if self.M("vulnerabilities").where("plugin=?",(plguin,)).count()>0:
vlun_infos=self.M("vulnerabilities").where("plugin=?",(plguin)).select()
vlun_infos=self.let_identify(result["plugins"][i]["Version"],vlun_infos)
for j2 in vlun_infos:
if j2["vlun_status"]:
vlun = {"name": "", "vlun_info": "", "css": "", "type": "plugin", "load_version": "","cve": "","time":""}
vlun["load_version"]=result["plugins"][i]["Version"]
vlun["cve"]=j2["cve"]
vlun["slug"]=plguin
vlun["name"] = Name
vlun["vlun_info"]=j2["msg"]
vlun["css"]=j2["css"]
vlun["time"] = j2["time"]
vlun_list.append(vlun)
return vlun_list