Initial YakPanel commit
This commit is contained in:
599
class_v2/projectModelV2/base.py
Normal file
599
class_v2/projectModelV2/base.py
Normal 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
|
||||
935
class_v2/projectModelV2/btpyvm.py
Normal file
935
class_v2/projectModelV2/btpyvm.py
Normal 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)
|
||||
|
||||
8
class_v2/projectModelV2/common/__init__.py
Normal file
8
class_v2/projectModelV2/common/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from .limit_net import LimitNet
|
||||
from .redirect import Redirect
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LimitNet",
|
||||
"Redirect"
|
||||
]
|
||||
47
class_v2/projectModelV2/common/base.py
Normal file
47
class_v2/projectModelV2/common/base.py
Normal 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)
|
||||
239
class_v2/projectModelV2/common/limit_net.py
Normal file
239
class_v2/projectModelV2/common/limit_net.py
Normal 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')
|
||||
832
class_v2/projectModelV2/common/redirect.py
Normal file
832
class_v2/projectModelV2/common/redirect.py
Normal 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)
|
||||
58
class_v2/projectModelV2/dockerModel.py
Normal file
58
class_v2/projectModelV2/dockerModel.py
Normal 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
|
||||
742
class_v2/projectModelV2/monitorModel.py
Normal file
742
class_v2/projectModelV2/monitorModel.py
Normal 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/trend,pct保留两位小数"""
|
||||
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=pv,order=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
|
||||
2538
class_v2/projectModelV2/nodejsModel.py
Normal file
2538
class_v2/projectModelV2/nodejsModel.py
Normal file
File diff suppressed because it is too large
Load Diff
4366
class_v2/projectModelV2/pythonModel.py
Normal file
4366
class_v2/projectModelV2/pythonModel.py
Normal file
File diff suppressed because it is too large
Load Diff
697
class_v2/projectModelV2/quotaModel.py
Normal file
697
class_v2/projectModelV2/quotaModel.py
Normal 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
|
||||
3540
class_v2/projectModelV2/safe_detectModel.py
Normal file
3540
class_v2/projectModelV2/safe_detectModel.py
Normal file
File diff suppressed because it is too large
Load Diff
4598
class_v2/projectModelV2/safecloudModel.py
Normal file
4598
class_v2/projectModelV2/safecloudModel.py
Normal file
File diff suppressed because it is too large
Load Diff
2685
class_v2/projectModelV2/scanningModel.py
Normal file
2685
class_v2/projectModelV2/scanningModel.py
Normal file
File diff suppressed because it is too large
Load Diff
377
class_v2/projectModelV2/totle_db.py
Normal file
377
class_v2/projectModelV2/totle_db.py
Normal 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
|
||||
|
||||
1664
class_v2/projectModelV2/webbasicscanningModel.py
Normal file
1664
class_v2/projectModelV2/webbasicscanningModel.py
Normal file
File diff suppressed because it is too large
Load Diff
278
class_v2/projectModelV2/wordpress_scan.py
Normal file
278
class_v2/projectModelV2/wordpress_scan.py
Normal 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
|
||||
Reference in New Issue
Block a user